上周在一台 Ubuntu 22.04 的机器上编译一个 2018 年留下的老项目,make 一跑就给我糊了一屏报错,什么-Werror=format-security、什么multiple definition of全冒出来了。查了半天才发现,源码没问题,是系统自带的 GCC 11 太"新"了。这种场面做过几年底层开发或者搞过嵌入式、CUDA 的人应该都不陌生:Ubuntu 切换 GCC 版本这件事,看着简单,真上手折腾的时候坑一个接一个。有人改完软链接发现gcc --version还是旧的,有人装了新版本却怎么都调不起来,还有人切完之后 C++ 链接报一堆 ABI 错误。
这篇东西就是把这些年我在 Ubuntu 上折腾 GCC 多版本的经验整一整:从为什么要切、系统里这些版本到底藏在哪、怎么装、怎么切、切完不生效怎么查,一直到构建系统里怎么把编译器钉死。不管你是刚在虚拟机里装完 Ubuntu 系统的新手,还是被某个第三方 SDK 逼着降编译器的老手,这里面的步骤基本都能直接抄。前提只有一个:你得知道自己在动什么,因为编译器是整条工具链的入口,动它一个,后面一串东西都会跟着变。
1. 先想明白:Ubuntu 下为什么非要切换 GCC 版本
1.1 系统预装的 GCC 是给系统用的,不是给你用的
Ubuntu 每个发行版都会绑定一个 GCC 主版本,装完系统预装的就是它,你什么都不用装。大致对应关系是这样的:Ubuntu 18.04 预装 GCC 7,20.04 是 GCC 9,22.04 是 GCC 11,24.04 则到了 GCC 13。这个默认版本是发行版维护者为了编译内核模块、系统组件、驱动源码而选定的,它的服务对象是"系统自身",诉求是稳、保守、能兼容绝大多数已有组件。
而你的项目诉求经常跟它是反的。系统组件希望编译器别乱加新警告,你的项目可能恰恰需要 C++20 的 concept、ranges 或者 C11 之后的新特性;反过来,一个 2016 年的老代码库放到 GCC 11 上,光是警告升级成错误就能让编译直接挂掉。两边打架,最后只能是你让步——把项目用的编译器切到合适的版本,而不是把整个系统降级。
1.2 最常触发切换的三类场景
第一类是老项目在新编译器上编译失败。这类最典型的表现是-Werror加了一堆新的警告类型,或者 GCC 10 之后默认的-fno-common让原本能过的"多个文件里重复定义全局变量"直接报multiple definition。代码一行没改,换个编译器就编译不过。
第二类是新代码需要新标准。老版本 GCC 对新标准的支持是分阶段补齐的,比如你想用 C++17 的std::optional、结构化绑定,GCC 7 就支持得很勉强,得升到 GCC 8 甚至 9 才舒服。做算法、写现代 C++ 的,经常被这个逼着升级。
第三类是第三方工具链对编译器版本有硬约束。这个在 GPU 计算和嵌入式里最明显:某些 CUDA 版本对宿主 GCC 有明确的上限,超出就报unsupported GNU version;某些芯片原厂的 SDK 只在自己验证过的 GCC 版本上保证行为一致。这时候切换不是"想切",是"必须切"。
1.3 切换、升级、上容器,三条路怎么选
遇到版本不对,第一反应别急着改系统,先看看这三条路哪条成本最低。
| 方案 | 适用场景 | 优点 | 代价 |
|---|---|---|---|
| 多版本共存 + 切换默认 | 一台机器上跑多个项目 | 系统不动,随时切回来 | 需要维护 alternatives |
| 直接升级系统 GCC | 只有一个项目、无所谓兼容 | 一步到位 | 可能拖垮系统组件编译 |
| 用容器 / 独立工具链 | 项目之间依赖严重冲突 | 环境彻底隔离,可复现 | 需要额外磁盘和一层学习成本 |
我的建议是:只要你不是只有一个项目,就选多版本共存。Ubuntu 的包管理本身就支持同一个 GCC 的多个主版本并存,gcc-9、gcc-11、gcc-13可以同时躺在/usr/bin下面互不干扰,真正"冲突"的只是那个不带版本号的默认入口。把默认入口交给update-alternatives管,比手改软链接靠谱得多。
2. Ubuntu 里 GCC 的"版本"到底藏在哪
2.1 /usr/bin/gcc 只是一个符号链接
很多人以为/usr/bin/gcc是个真实文件,其实它只是一层跳转。链路的完整形态是这样:/usr/bin/gcc指向/etc/alternatives/gcc,而/etc/alternatives/gcc再指向真正的可执行文件,比如/usr/bin/gcc-11。中间这一层/etc/alternatives就是 alternatives 机制的落地目录,它的存在意义是让你改一处、全局生效。
你可以自己验证一下:
readlink -f $(which gcc)输出的如果是/usr/bin/gcc-11这种带版本号的名字,说明切换机制是正常工作的。要是输出就是/usr/bin/gcc本身,那说明这个 gcc 可能是手工拷进去的,或者被某个 SDK 覆盖过,这种环境下改 alternatives 是不会生效的,得先把路径理清楚。
2.2 版本化二进制和它的全家桶
关键是要建立一个概念:GCC 不是一个程序,是一整套。装gcc-11和g++-11的时候,实际落地的文件远不止两个:
ls /usr/bin/ | grep -E '^(gcc|g\+\+|cpp|gcc-ar|gcc-nm|gcc-ranlib)-[0-9]+$'你会看到gcc-11、g++-11、cpp-11、gcc-ar-11、gcc-nm-11、gcc-ranlib-11这一串。另外还有 triplet 前缀的一批,比如/usr/bin/x86_64-linux-gnu-gcc-11,这些是给交叉编译和构建系统用的别名。真正的编译器内部程序(cc1、cc1plus、collect2)不在/usr/bin,而是在/usr/lib/gcc/x86_64-linux-gnu/11/这样的目录下。
理解这一点很重要:切换版本要切的是一组链接,不是单个文件。只把gcc切了、g++没切,C 代码没事,C++ 代码就会出问题,因为编译 C++ 时调用的是g++,它背后的标准库头文件路径和gcc不是一回事。
2.3 头文件和库文件也分版本
编译器版本不同,它自带的头文件目录和运行时库也分版本。头文件在/usr/lib/gcc/x86_64-linux-gnu/<版本>/include/,这是个会被自动加入搜索路径的目录。C++ 标准库头文件则在/usr/include/c++/<版本>/,比如/usr/include/c++/11/。切换g++的时候,g++-11会自动指向/usr/include/c++/11/,这也是为什么必须整体切换的原因。
还有一个容易被忽略的点:libstdc++.so.6是所有 GCC 版本共用的一个 C++ 运行时库,它靠 GLIBCXX 符号版本做向后兼容。你可以这样看它支持到哪个版本:
strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX | tail -5如果旧编译器编出来的二进制跑在新系统上提示version GLIBCXX_3.4.29 not found,那就是这个库的前向兼容问题,方向恰好相反——在旧系统上编译、到新系统上跑,通常没事;反过来就有风险。
2.4 GCC 各主版本之间的行为差异才是真正的雷
这里我列几个实际项目里最容易炸的差异,都是踩过才知道的:
- GCC 10 起默认开启
-fno-common。C 语言里int flag;这种写法如果在多个 .c 文件里都出现,以前能过,GCC 10 之后直接multiple definition。修法是把定义改成extern,只在某一个文件里真定义。 - 默认 C++ 标准在变。GCC 11 起默认
-std=gnu++17,之前是gnu++14。代码里如果有依赖旧默认行为的地方,换了编译器表现就不一样。 - 警告体系在升级。
-Wformat-security、-Warray-bounds、-Wmaybe-uninitialized这些在不同版本下误报率和严格度都不同,配合-Werror就是编译不过。 - GCC 14 把部分警告变成了错误,包括隐式函数声明等,老 C 代码在 Ubuntu 24.04 上直接编不动的情况我见过好几次。
搞清楚这些差异,你就明白为什么有些项目"换个编译器就坏",也明白为什么切换前最好先用gcc -v看一眼当前的配置参数。
3. 准备工作:查家底、装齐需要的版本
3.1 三条命令摸清现状
动手之前先做体检,别上来就装。第一条看当前默认版本:
gcc --version g++ --versiongcc -v会多输出编译时的配置参数,比如--enable-languages、线程模型、target 三元组,做交叉编译排查时这个信息很有用。第二条看已经装了哪些版本:
ls -l /usr/bin/gcc* | grep -v '\->' dpkg -l | grep -E '^ii\s+(gcc|g\+\+)-[0-9]'第三条看 alternatives 当前登记了哪些候选:
update-alternatives --list gcc update-alternatives --display gcc--display的输出会明确告诉你当前是auto模式还是manual模式、每个候选的优先级、以及当前指向哪个路径。这三条命令的输出建议截图存一下,切坏了能快速对照恢复。
3.2 从官方源安装需要的版本
Ubuntu 官方源里通常保留了相邻的几个主版本,不需要加任何第三方源。直接装:
sudo apt update sudo apt install -y gcc-9 g++-9 sudo apt install -y gcc-12 g++-12具体源里有哪些,用这条命令确认最准:
apt-cache search '^gcc-[0-9]+$' | sort装的时候有一个硬性要求:gcc-N和g++-N版本号必须一致,别出现gcc-9配g++-11这种组合。原因前面讲过,C++ 的头文件路径和链接时用的标准库都是跟着g++版本走的,混搭会引入很难查的 ABI 类问题,症状可能是运行期莫名的段错误或者符号找不到。
如果还想装调试器和配套工具,gdb、binutils一般不用动,它们跟 GCC 主版本没有强绑定关系,除非有特殊需求,否则保持系统默认就行。
3.3 源里没有所需版本怎么处理
碰到特别老(比如 GCC 4.8)或者特别新的版本,官方源里找不到,通常有两条路。一条是去翻 Ubuntu 的旧版本仓库(old-releases),把对应的源加进/etc/apt/sources.list再装——这条路我一般只在必须复现老环境时才走,因为混源容易把依赖关系搞乱。另一条更干净:直接下预编译的工具链包,解压到/opt下独立使用,完全不碰系统目录。
第二条路的具体做法是这样的:解压到/opt/toolchains/gcc-x.y.z,然后在使用时通过CC、CXX环境变量或构建系统参数指定绝对路径。这种方式不污染系统、随时删掉、多个版本可以并存好几套,唯一的代价是每次都得显式指定,不能靠"默认"生效。我个人的习惯是:能用 apt 装的就用 apt,实在装不了的才走独立工具链,因为 apt 装的版本会被系统统一管理,升级、卸载都省事。
4. 首选方案:用 update-alternatives 管默认版本
4.1 它到底解决了什么问题
update-alternatives是 Debian 系特有的机制,专门解决"同一类程序有多个版本、需要一个默认入口"的问题。它的核心价值是:所有跳转都集中在/etc/alternatives下面,切换是原子的,可回退,可查询。你不用去记哪个软链接指向哪,也不用担心手改软链接之后忘了原来指向谁。
它还有个--slave机制,能把一组相关的命令绑成一个"主从组"。切gcc的时候,g++、cpp、gcc-ar这些从链接跟着一起切。这一点对 GCC 尤其重要,因为前面说过 GCC 是一整套工具。
4.2 完整可复制的配置命令
假设你现在装好了gcc-9和g++-9,想把它登记进 alternatives 体系,命令是这样:
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 90 \ --slave /usr/bin/g++ g++ /usr/bin/g++-9 \ --slave /usr/bin/cpp cpp /usr/bin/cpp-9 \ --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-9 \ --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-9 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-9参数的含义拆开看:/usr/bin/gcc是主链接路径,gcc是 alternatives 组的名字,/usr/bin/gcc-9是实际候选文件,90是优先级。后面每个--slave三个参数依次是"从链接路径、从链接组名、实际文件路径"。
按同样的格式把其他版本也登记进去,只是优先级数字换一下,比如gcc-11用 110,gcc-13用 130。
登记完之后,自动模式(auto)会选优先级最高的那个。如果你想手动指定,用交互模式:
sudo update-alternatives --config gcc它会列出所有候选并编号,输入序号回车即可。切换之后/etc/alternatives/gcc会指向你选的那个,立即生效,不需要重启任何东西。
4.3 优先级数字怎么定才不闹心
优先级只有一个规则:数字越大越优先,只在 auto 模式下起作用。一旦你手动--config选过一次,这个组就变成 manual 模式了,后续新装的候选不会自动抢占。
我的习惯是把"日常最常用、最稳定"的那个版本优先级设得最高,比如你大部分项目都用 GCC 11,就把它的优先级定成 110,其他版本定低一点。这样即便以后又--install进来一个新版本,只要它的优先级低于 110,auto 模式也不会把你现在的默认版本换掉。踩过的坑是:有人给新装的版本随手填了个很大的优先级,结果某次 apt 升级后默认编译器悄悄变了,第二天上班编译报错才发现。
想恢复成自动模式:
sudo update-alternatives --auto gcc想彻底移掉某个候选:
sudo update-alternatives --remove gcc /usr/bin/gcc-94.4 顺手确认一下 cc 是不是也跟着走了
顺手确认一下cc是不是也跟着走了。/usr/bin/cc通常是另一条 alternatives 记录,链路是/usr/bin/cc→/etc/alternatives/cc→/usr/bin/gcc,最终会跟着主链接一起变。但在某些系统或者被第三方 SDK 改过之后,它可能指向别处,所以切完还是确认一下比较稳:
readlink -f $(which cc)如果它没跟着走,可以单独给它建一条记录:
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc 1005. 备选方案:软链接、环境变量和编译时指定
5.1 直接改软链接:快,但风险也最高
最原始的办法就是把链接删了重建:
sudo rm /usr/bin/gcc sudo ln -s /usr/bin/gcc-9 /usr/bin/gcc这么做确实立刻生效,但问题很明显。第一,你绕过了包管理系统,下次 apt 升级或者装新包的时候,这个链接随时可能被覆盖回去,而且不会有任何提示。第二,原来指向/etc/alternatives/gcc的那层不见了,等于把 alternatives 体系破坏了,以后--config也不管用了。第三,出问题的时候你很难记住自己改过什么。
所以这条路我只在两种情况下用:一是临时验证某个版本能不能解决编译问题,用完立刻恢复;二是系统里本身就没有 alternatives 体系(比如某些精简容器镜像),那也只能这么做。
5.2 用 PATH 做用户级切换,不动系统
比改系统链接干净得多的做法是在用户级做切换。思路是准备一个自己的 bin 目录,放一组 wrapper 脚本,然后把它加到 PATH 最前面。
mkdir -p ~/toolchains/bin cat > ~/toolchains/bin/gcc <<'EOF' #!/bin/bash exec /usr/bin/gcc-9 "$@" EOF chmod +x ~/toolchains/bin/gccg++、cpp同理各写一个,然后:
echo 'export PATH=$HOME/toolchains/bin:$PATH' >> ~/.bashrc这样做的最大好处是完全不动系统,只影响你自己的 shell 会话,换台机器或者换个用户就不生效了,出问题删目录即可。
这里有个细节必须说清楚:不要直接把/usr/bin/gcc-9软链接到~/toolchains/bin/gcc。GCC 启动后会根据自己的可执行文件路径去推导内部程序(cc1、cc1plus)和运行时库的位置,走软链接可能推导到错误的前缀,报cannot execute 'cc1plus'这类错。用 wrapper 脚本exec真实路径,等于骗过了这层推导,稳妥得多。
5.3 编译时显式指定:工程上最推荐
如果你有权限改构建脚本,那最推荐的其实是不做全局切换,直接在编译时指定。这样每个项目各用各的编译器,互相不干扰,也不会因为某天系统默认版本变了导致构建结果漂移。
Makefile 里是这样:
CC = gcc-9 CXX = g++-9CMake 里是配置阶段传参:
cmake -S . -B build \ -DCMAKE_C_COMPILER=/usr/bin/gcc-9 \ -DCMAKE_CXX_COMPILER=/usr/bin/g++-9这种方式的代价是每处都要写,好处是可复现性极强。CI 上跑构建、别人拿到代码复现你的环境,都不需要额外说明"记得先切 GCC 版本",脚本自己就说清楚了。做长期维护的项目,我一律推荐走这条路,全局切换只作为本地临时调试用。
6. 切完了却还是旧版本?六种情况逐个排查
这个标题本身就是一个高频问题——gcc升级后为啥还是旧版本。绝大多数情况不是没切成功,而是你看的、用的、构建系统记录的不是同一个东西。下面按排查顺序列。
6.1 第一种:shell 的命令哈希缓存
bash 会把已经执行过的命令路径缓存起来,避免每次都去 PATH 里查找。如果你是通过改 PATH 的方式切换,缓存可能还指着旧路径。
hash -r # 或者 rehash执行完再which gcc看一次。这个坑在 zsh 下也存在,对应命令是rehash。判断方法:which gcc和type gcc输出不一致,基本就是缓存问题。
6.2 第二种:cc 和 gcc 不是一回事
有些构建脚本用的是$(CC),而CC默认值是cc而不是gcc。你切了gcc,cc没动,脚本自然还是用旧的。排查方法:
echo $CC readlink -f $(which cc)如果CC在某个环境变量文件里被写死了,那 alternatives 怎么切都没用。这种情况优先改环境变量,而不是继续折腾链接。
6.3 第三种:CMake 把编译器路径写死在缓存里了
CMake 在第一次配置时会把编译器路径存进CMakeCache.txt。之后你哪怕改了系统默认编译器,只要 build 目录还在,CMake 就继续用缓存里的那个。
grep -E 'CMAKE_(C|CXX)_COMPILER' build/CMakeCache.txt解决办法有两个:删掉整个 build 目录重新配置(最干净),或者在缓存里改:
cmake -S . -B build -DCMAKE_C_COMPILER=/usr/bin/gcc-9注意,如果缓存里已经写死了另一个路径,直接传新参数有时会被忽略,最保险还是删缓存目录。这个坑我在接手别人项目时遇到不止一次,排查了半天以为是编译器没切成功,其实是 CMake 一直在用旧缓存。
6.4 第四种:环境变量 CC / CXX 覆盖了默认
优先级大体是:构建系统显式参数 >CC/CXX环境变量 > 系统默认。所以CC一旦被设上,update-alternatives切什么都是白搭。检查一下:
env | grep -E '^(CC|CXX|CFLAGS|CXXFLAGS)='检查的地方包括~/.bashrc、~/.profile、/etc/environment、以及某些 SDK 的setup.sh。有些 SDK 会在初始化脚本里export CC=gcc,你一 source 就把环境带偏了。
6.5 第五种:交叉编译工具链抢了 PATH 前面
装过 ARM、RISC-V 或者某些原厂 SDK 的机器上,PATH最前面往往挂着一整套交叉工具链。这些工具链里也有个叫gcc的文件(虽然通常是arm-none-eabi-gcc,但有些会提供gcc别名),一执行就跑到交叉编译器上去了。
which -a gccwhich -a会把 PATH 里所有匹配的都列出来,看一眼顺序就清楚了。这种情况的正确做法不是继续加切换,而是把这些工具链的初始化脚本从.bashrc里移出去,改成按需 source。
6.6 第六种:你改了 gcc,但链接器和头文件没跟着走
这种情况比较隐蔽:gcc --version显示是对的,但编译出来的东西行为就是不对。原因可能是g++没跟着切,或者ld、as这些 binutils 组件是另一个版本。判断方法是对比一下这几个:
gcc --version | head -1 g++ --version | head -1 ld --version | head -1正常情况gcc和g++的主版本应该一致。如果g++还是旧的,那就是前面--slave没配对,回去补上。另外 C++ 项目里还可以直接看头文件搜索路径:
echo | g++ -x c++ -E -v - 2>&1 | grep '/usr/include/c++'输出里带的版本号应当和你要用的编译器版本对应。
7. 构建系统里怎么把编译器钉死
7.1 CMake:在工具链文件里统一声明
项目里长期用某个特定编译器,比每次敲参数更规范的做法是写一个工具链文件。新建cmake/toolchain-gcc9.cmake:
set(CMAKE_C_COMPILER /usr/bin/gcc-9) set(CMAKE_CXX_COMPILER /usr/bin/g++-9) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON)配置的时候指定:
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-gcc9.cmake工具链文件的好处是它同时约束了 C 和 C++ 编译器,还顺手把语言标准也钉住,避免不同机器上因为默认标准不同导致行为漂移。这套做法在需要严格复现构建结果的场合几乎是标配。
7.2 Makefile:做一个可覆盖的默认值
Makefile 里最容易犯的错是把CC写死成=赋值,这样外部传入的环境变量会被覆盖。正确的写法是用?=:
CC ?= gcc CXX ?= g++ CXXFLAGS ?= -O2 -Wall -std=c++14这样默认走系统 gcc,需要的时候在命令行覆盖:
make CC=gcc-9 CXX=g++-9用?=的意义在于,别人不用你的环境也能编,你也不用为了编个代码去改 Makefile。这个小细节在实际协作里省的事非常多。
7.3 CUDA 场景:用 -ccbin 指定宿主编译器
用 nvcc 编译的时候,它会调用宿主的 gcc 来编译 host 侧的代码。如果宿主 gcc 版本超出 nvcc 支持范围,就会报unsupported GNU version。这时候不是去降系统 gcc,而是明确告诉 nvcc 用哪个:
nvcc -ccbin /usr/bin/gcc-9 -o app main.cuCMake 里对应设置CMAKE_CUDA_HOST_COMPILER:
cmake -S . -B build \ -DCMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-9这样切换只影响 CUDA 项目,其他项目照常用系统默认,互不打扰。顺便说一句,那种"加参数强行绕过版本检查"的做法我不太推荐,版本检查是有原因的,绕过之后出问题会更难查。
8. 常见报错与切换问题速查表
把前面几节提到的症状和对应处理整理成表,方便对着查。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
gcc --version还是旧版本 | shell 哈希缓存 / PATH 顺序 | hash -r,再用which -a gcc看顺序 |
| CMake 构建用的是旧编译器 | CMakeCache.txt里写死了路径 | 删 build 目录重新配置,或显式传CMAKE_C_COMPILER |
| 脚本报编译器版本不符 | CC/CXX环境变量覆盖 | 检查 `env |
| C++ 链接期报符号找不到 | g++与gcc版本不配套 | 补上--slave,确保主从组一致 |
multiple definition of | GCC 10+ 默认-fno-common | 把定义改extern,或临时加-fcommon |
unsupported GNU version | nvcc 宿主编译器版本超出范围 | 用-ccbin指定受支持的 gcc |
运行时GLIBCXX_x.x.xx not found | 编译机标准库比运行机新 | 降编译版本,或升级运行环境标准库 |
找不到cc1plus | 软链接方式破坏了 GCC 路径推导 | 改用 wrapper 脚本或 alternatives |
| 编译速度突然极慢 | 误用了带调试信息的老版本 | 检查实际调用的编译器路径 |
表格之外补一句:真正的排查顺序建议固定成"which -a看顺序 →readlink -f看真实路径 →env看环境变量 → 看构建系统缓存"。这四步走完,九成以上的"切了没生效"都能定位。
9. 我在实操中攒下的一些习惯和心得
折腾这些年,有几个习惯我觉得比命令本身更值钱,一并写在这儿。
第一个是切换前先记录当前状态。切换前把update-alternatives --display gcc和readlink -f $(which gcc)的输出存到一个文本文件里,出问题了照着往回改。我踩过的最难受的一次坑是把 alternatives 组删了一半,结果--config列表空着,只能靠记忆手动--install重建。有记录就完全不是问题。
第二个是永远优先用 smallest blast radius 的方案。能用工具链文件解决就不改系统默认,能用环境变量解决就不改软链接,能改软链接就不动/usr/bin下的真实文件。原因很简单:系统目录一改,影响的是所有用户、所有项目,包括系统自身的构建脚本和后续的包升级,连锁反应比想象中大。
第三个是把版本信息写进项目文档或者构建脚本里。见过太多"在我机器上能编,在你机器上编不过"的扯皮,最后查出来就是编译器版本不同。项目根目录放一个TOOLCHAIN.md,写清楚需要哪个版本、怎么装、怎么切,成本极低,收益很高。CI 镜像里也建议直接固定好版本,别依赖 runner 的默认环境,否则哪天 runner 升级了,构建莫名其妙就红了。
最后补充一个实操小技巧:如果你需要频繁在几个 GCC 版本之间来回切,可以写个 shell 函数封装一下,省得每次敲一长串update-alternatives。
switch_gcc() { local ver="$1" sudo update-alternatives --set gcc "/usr/bin/gcc-${ver}" sudo update-alternatives --set g++ "/usr/bin/g++-${ver}" 2>/dev/null hash -r gcc --version | head -1 }前提是这些版本都已经--install登记过。用--set而不是--config,是因为它不带交互,能直接放进脚本或 alias 里。切完顺手打印一行版本号,避免你以为切了其实没切——这个习惯帮我省过好多次误判的时间。