gfortran和gcc的降级安装中的那些坑
前阵子手贱跑了一轮apt update && apt upgrade,把服务器上的gfortran从9系列直接抬到了11系列,当时没当回事,结果第二天一编译旧项目,满屏的报错直接把我看懵了:unrecognized command-line option、undefined reference、CMake找不到Fortran编译器,接二连三地冒出来。折腾了一整天,最后老老实实把gfortran和gcc降级回去,才算消停。这篇文章就把这次降级踩过的坑、试过的路子和最终能稳定复现的操作流程完整记下来,给后面遇到类似情况的各位一个参考。
如果你也遇到"系统自带的gfortran/gcc版本太新,旧项目编译不过"、"OpenFOAM或其他科研计算程序对编译器版本有硬性要求"、"装完新版本后连mpif90都跟着乱了套"这类问题,这篇文章就是为你准备的。我不讲虚的,只讲实际能跑通的命令和思路,以及每一步背后的原因。
1. 为什么好好的系统非得降级:先搞清楚问题的本质
1.1 升级之后出现的典型症状
先说最常见的场景。很多科研和工程环境里的Fortran程序,尤其是那些写于几年前、后期不再维护的代码,对编译器版本的依赖非常敏感。升级gfortran/gcc表面上看是小事,但项目一编译就会暴露问题。
以我当时遇到的情况为例,症状非常典型:
- 老项目里用了旧版编译器允许的
-m32、-fdefault-real-8等选项,新编译器直接报unrecognized command-line option。 - CMake配置阶段提示
The Fortran compiler identification is unknown,直接卡在编译配置上。 - 偶尔有编译能过的,运行阶段又会因为libgfortran版本不对出现
version 'GFORTRAN_9' not found之类的错误。
这些问题的根源,基本都能归结到一点上:编译器本身的语法规则、内部库的ABI接口在版本之间发生了变化,旧代码没有跟上新编译器的节奏。
1.2 降级前必须确认的三件事
在动手降级之前,别急着删包,先花两分钟确认以下信息,能省掉后面一大堆麻烦:
- 当前实际使用的版本:执行
gfortran --version、gcc --version,看清楚现在到底是多少。 - 目标版本是什么:去旧项目的构建文档或
CMakeLists.txt里翻一下,看它明确要求哪个版本范围。比如很多老项目要求gfortran-9或gfortran-8。 - 系统repo里能装到什么版本:执行
apt-cache policy gfortran-9 gcc-9,看源里是否还有低版本的包。这一步很关键,直接决定你后面用哪种方案。
我第一次就没做第3步,以为直接apt install gfortran-9就能搞定,结果系统告诉我没有可安装的候选版本,白白折腾了一阵。不同发行版的源配置千差万别,有的低版本包还在,有的早就被清理了。
1.3 gfortran和gcc必须成对降级
这里有个容易忽略的坑:gfortran和gcc在同一个项目里经常是配套使用的,降级的时候必须保持主版本号一致。比如你降级到gfortran-9,gcc最好也降到gcc-9。CMake在检测Fortran编译器时,会通过gfortran -c测试编译一个C和Fortran混合的链接程序,如果C编译器是gcc-13、Fortran编译器是gfortran-9,很容易出现底层libgfortran和libgcc之间的兼容性断裂,表现就是各种诡异的链接错误。
举个例子,Fortran代码调用C的数学库函数时,gfortran会在编译时自动链接libgfortran和libquadmath,如果C编译器版本对应的libgcc与gfortran的ABI不兼容,链接阶段会报cannot find -lgfortran或者一堆莫名其妙的undefined reference,但单独编译C或单独编译Fortran又都没问题。我这次就被这个现象迷惑了好一阵,最后才发现是gcc还停留在高版本、gfortran却降下去了,两者错配导致。
2. 三条降级路径的对比:apt、deb包、源码编译怎么选
先把结论放在前面:能用apt直接从源里装旧版本就用apt,这是最省心的路;源里没有就尝试下载旧版deb包手动安装;源码编译是最后手段,适合需要精确控制小版本号的场景。
2.1 路径一:apt仓库直装
这是最理想的情况。如果系统源里还保留着旧版本包,直接装就行,依赖关系会自动处理。比如在Ubuntu 22.04上安装gfortran-9:
sudo apt update sudo apt install gfortran-9 gcc-9装完之后,系统里会同时存在多个gfortran版本,默认的gfortran命令可能还是指到新版本,需要通过update-alternatives来切换。这个我在第三节里会专门展开。
适用条件:目标版本在源里还能找到对应包,且不需要精确到某个补丁版本。
2.2 路径二:手动下载deb包
如果源里没有,但你知道某个旧版本编译成的deb包在其他源或镜像站还能找到,可以手动下载安装。这种方式的关键在于理顺依赖关系。
以gfortran-9为例,它依赖gcc-9、libgfortran-9-dev、libgcc-9-dev等一堆底层包。直接dpkg -i单个deb经常报依赖缺失,所以更好的做法是:
# 先把需要的deb包下载到本地目录 mkdir ~/gcc-old && cd ~/gcc-old wget http://mirrors.xxx/.../gfortran-9_9.x.x_amd64.deb wget http://mirrors.xxx/.../gcc-9_9.x.x_amd64.deb # ...其他依赖包 # 然后用dpkg尝试安装,缺什么补什么 sudo dpkg -i *.deb如果中途报依赖错误,用sudo apt -f install修复一下,它会自动把缺失的依赖从当前源里装好。这一步也是我自己实际操作中发现最有效的补依赖方式。
适用条件:能找到旧版deb包,且系统里的基础库版本与deb包的依赖要求不冲突。
2.3 路径三:源码编译安装
这是控制力最强、但也最费时费力的方案。适用于apt和deb都搞不定、或者你需要一个特定小版本号(比如9.4.0而不是9.3.0)的场景。
编译GCC的完整流程我简单提炼如下:
# 1. 下载源码 wget https://mirrors.xxx/gcc/gcc-9.4.0/gcc-9.4.0.tar.gz tar -xzf gcc-9.4.0.tar.gz cd gcc-9.4.0 # 2. 下载GCC编译所需要的依赖(gmp、mpfr、mpc) ./contrib/download_prerequisites # 3. 编译配置 mkdir build && cd build ../configure --prefix=/opt/gcc-9.4.0 \ --enable-languages=c,c++,fortran \ --disable-multilib \ --disable-bootstrap # 4. 编译并安装 make -j$(nproc) sudo make install这里要特别提醒几个容易踩的坑:
--disable-bootstrap建议加上,它可以省掉三遍自举编译的时间。如果你机器性能一般,完整自举可能要两三个小时起步,加上这个选项能省不少。--disable-multilib建议加上,除非你真的需要编译32位和64位两套库。不加的话,configure阶段可能会因为缺少32位库的依赖而报错。- 安装完成后,
gfortran在/opt/gcc-9.4.0/bin下,需要手动加入PATH或者用软链接接到/usr/local/bin使用。
源码编译的明显缺点是编译时间长、依赖步骤多,但它能装出完全独立的工具链,不受系统源版本的约束。我在另一台没有旧版包的机器上就是这么干的,装完用/opt/gcc-9.4.0/bin/gfortran指定编译,完全不干扰系统自带的编译器。
2.4 三条路径的选择建议
| 路径 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| apt直装 | 快、稳、依赖自动处理 | 版本选择受限 | 源里还有旧版本 |
| 手动deb | 版本可精确控制 | 依赖比较麻烦 | 源里没有、能找到旧deb |
| 源码编译 | 版本完全可控、可独立安装 | 耗时长、技能要求高 | 需要极端精确的编译环境 |
3. 最容易翻车的三个深水区:库不匹配、mpif90残留、多版本共存
如果说降级安装只是"装回去"那倒简单了,真正的坑往往藏在装完之后的系统状态里。我这一次折腾下来,最深的三处雷分别是:libgfortran动态库的版本错位、mpif90包装器的残留指向、以及多版本共存时命令到底指向谁。
3.1 libgfortran.so版本错位:编译能过但运行就崩
这个坑是最隐匿的,因为它不发生在编译阶段,而是发生在你满怀期待运行程序的那一刻。典型报错是这样的:
./program: error while loading shared libraries: libgfortran.so.4: cannot open shared object file: No such file or directory或者:
./program: /usr/lib/x86_64-linux-gnu/libgfortran.so.5: version 'GFORTRAN_9' not found (required by ./program)这两种报错本质是同一个问题:程序编译时依赖的libgfortran库和你当前系统里安装的libgfortran库不是同一个大版本。
这里需要理解一个背景:gfortran编译出的程序在运行时会动态链接libgfortran.so,而这个小版本号(比如.so.4、.so.5)对应的是不同代际的ABI接口。用gfortran-7/8编译出来的程序需要libgfortran.so.4,用gfortran-9及更高版本编译出来的程序则需要libgfortran.so.5。如果你把gfortran降级到了9,但系统里的libgfortran还停留在新版本,运行旧版gfortran编译出的程序就会找不到对应的.so.4,或者反过来版本号对不上。
解决办法很简单,和gfortran降级配套执行:
# 先看系统当前装了哪个版本的libgfortran dpkg -l | grep libgfortran # 把与目标gfortran配套的运行时库装上 sudo apt install libgfortran-9-dev如果项目里有些程序是旧编译器编译的、有些是新编译器编译的,那系统里新旧版本libgfortran共存是很正常的,不用担心冲突。它们本身是不同so文件名,可以共存。
我当时的教训是:只降级了gfortran命令,没管libgfortran,结果用降级后的gfortran重新编译旧项目时一切正常,但项目里有些预编译好的第三方库是用更老版本编译的,一运行就报上面那种错。后来把libgfortran-9-dev装上,问题立刻消失。
3.2 mpif90的残留指向:降级后MPI程序编译异常
这个坑我必须单独花一整节来说,因为它几乎要了我的命。
在安装了OpenMPI或者MPICH的机器上,日常编译MPI并行的Fortran程序用的是mpif90命令。但很多人不知道的是,mpif90本质上是一个包装脚本,它在安装时会把自己绑到当时系统里存在的那个gfortran上,并且把编译器路径和版本信息固化在脚本内部。
你降级gfortran之后,mpif90可能还指向旧的高版本gfortran。这时候用mpif90编译程序,表面上看到的是mpif90在干活,实际上它背后调用的还是那个旧版编译器,于是:
- CMake检测环境时,
CMAKE_Fortran_COMPILER可能被设置成mpif90,但实际生效的编译器还是旧版; - 编译出来的程序和其它用新(降级后)编译器编译的对象文件混在一起链接,各种符号对不上。
这个问题的发现过程很有趣。我降级后单独用gfortran编译一个简单的Fortran测试程序,完全正常。但一用mpif90编译项目,就出现类似mpif90: No such file or directory或者编译选项报错。一开始我还以为是降级没降干净,后来用mpif90 -show一查,才发现它指向的-f90=参数还是原来的gfortran-11路径。
解决办法有两个:
# 方法一:重装mpi库,让包装脚本重新绑定到当前默认的gfortran sudo apt install --reinstall openmpi-bin libopenmpi-dev # 方法二:如果是MPICH sudo apt install --reinstall mpich重装之后再用mpif90 -show查看,确认包装脚本里的编译器路径已经指向降级后的gfortran,再编译项目就一切正常了。
顺便说一下,如果你用的是mpicc、mpicxx,它们同理,也存在这个包装器绑定问题。降级gcc后最好也把mpi库重装一遍,别等到出了问题再排查。
3.3 update-alternatives没配好:gcc、gfortran命令到底指向谁
装完多版本编译器之后,你会在/usr/bin下看到gfortran-9、gfortran-11、gcc-9、gcc-11等一大堆命令。而gfortran、gcc这两个不带版本号的名字,是通过update-alternatives来决定的。
很多新手会直接改/usr/bin/gfortran软链接,这是个错误做法。因为apt的包管理机制会在升级时覆盖这个软链接,而且手动改软链接很容易把系统搞坏。正确做法是用update-alternatives:
# 注册备选版本 sudo update-alternatives --install /usr/bin/gfortran gfortran /usr/bin/gfortran-9 100 sudo update-alternatives --install /usr/bin/gfortran gfortran /usr/bin/gfortran-11 200 # 切换默认版本 sudo update-alternatives --config gfortrangcc同理:
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 100 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 200 sudo update-alternatives --config gcc括号里的数字代表优先级,数值大的优先级高,会在auto模式下被优先选中。手动执行--config后系统会问你要选哪个,直接输入对应序号即可。
一个需要注意的点是:update-alternatives并不只影响命令本身。CMake在检测编译器时,会调用gfortran这个命令来执行编译测试,所以只要/usr/bin/gfortran指向对了,CMake一般就能识别到正确的版本。反过来如果你只改了CMake里的编译器路径,但命令行直接输gfortran还是旧版,项目里有些Makefile写死调用gfortran的地方还是会出问题。所以两边都要确认一致。
4. 降级后的验证与固化:确认环境稳定不再被apt"偷摸"升级
4.1 三级验证法:从命令行、CMake到动态库层层确认
降级安装完成并不等于"完工"。我习惯用三层验证来确认环境真的稳了,强烈推荐大家都做一遍,防止后面编译到一半才暴露问题。
第一级:命令行版本检查。
gfortran --version gcc --version确认输出的版本是目标版本。同时用which gfortran看一下它实际指向的路径,确保不是被某个环境变量带偏了。
第二级:CMake实际检测。
在一个干净目录里写一个最小CMake项目,或者直接跑一次项目的CMake配置,重点看日志里这两行有没有出来:
-- The Fortran compiler identification is GNU 9.x.x -- Check for working Fortran compiler: /usr/bin/gfortran-9如果identification显示的还是新版本,说明CMake缓存了你之前配置过的编译器信息,把CMakeCache.txt删掉重新配置即可。这是另一个非常常见的坑,很多人明明改了编译器却忘了清缓存,结果CMake一直沿用旧配置。
第三级:动态库依赖检查。
编译出一个小的Fortran程序,用ldd看它的动态库依赖:
ldd ./hello_fortran正常应该能看到类似这样的输出:
libgfortran.so.5 => /usr/lib/x86_64-linux-gnu/libgfortran.so.5 libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1如果链接到了不存在的库版本,运行时会立刻报错。这一步能最直观地验证3.1节里提到的库错位问题是否已经解决。
4.2 用apt-mark锁住版本,防止未来被再次升级
这是我在这次操作里学到的最后也是最重要的一课。
你辛辛苦苦把版本降回去了、验证全部通过了,如果哪天手一滑又执行了apt upgrade,或者某个依赖包导致编译器被牵连升级,一切又得重来。为了避免这种情况,apt提供了一个版本锁定功能:
sudo apt-mark hold gfortran-9 gcc-9 sudo apt-mark hold libgfortran-9-dev libgcc-9-dev执行之后,用apt-mark showhold可以查看当前被锁定的包列表。以后apt upgrade的时候,这些包会被跳过,不会被替换成高版本。
需要说明的是,apt-mark hold锁住的是包本身,如果某个新包依赖更高版本的gfortran,apt会提示依赖冲突,这时候你要权衡一下是临时取消锁定装新包,还是继续锁着。在我的场景里,科研计算环境追求的是稳定,锁住完全合理。
4.3 如果以后想恢复新版本
降级不是永久决定。如果哪一天项目更新了、代码适配了新编译器,想恢复高版本,只需要:
sudo apt-mark unhold gfortran-9 gcc-9 libgfortran-9-dev libgcc-9-dev sudo apt update && sudo apt upgrade然后再把update-alternatives切回高版本即可。整个恢复过程和我前面讲的降级流程完全是镜像操作,思路一致。
5. 最后的经验总结
这次降级折腾下来,我个人最大的体会是:编译器降级的难点不在于"怎么把旧版本装回去",而在于装完之后整个工具链的配套状态是否一致。gfortran和gcc要配套,libgfortran运行时库要配套,mpif90这类包装器要重新绑定,CMake缓存要清掉,apt的自动升级要锁住。任何一个环节没对齐,编译阶段的报错都足够让人怀疑人生。
如果你准备动手,我的建议是:先花10分钟把第一节里的三件事确认清楚,再选一条路径开始操作,然后务必在装完后把第四节的三级验证完整跑一遍。这套流程走下来,虽然不能保证百分之百不踩坑,但至少能让你从"看天吃饭"变成"心里有谱"。
最后分享一个小技巧:降级和验证的过程中,建议把你执行的每一条命令、看到的关键报错都记在一个文本文件里。这听起来很基础,但实际排查多版本编译器问题时,来回切换版本、反复清理缓存,很容易搞混当前状态。有一条命令记录在手,出问题回溯起来效率高得多。