Ubuntu 18.04 离线装 gcc7:依赖、dpkg 顺序与多版本切换
简介:面向 Ubuntu 18.04 环境下需要离线部署 C/C++ 编译工具链的开发者与运维人员,这套压缩包把 gcc 7.3 及其配套依赖整理成可直接用 dpkg 安装的本地材料,主要解决无法访问外网、内网机房或在线软件源失效时难以安装编译器的问题。包内共 25 个文件,其中 24 个 deb 包构成主体,涵盖以 gcc-7、cpp-7、binutils 为核心的编译器组件,以 libgcc-7-dev 为代表的开发头文件,libasan4、libtsan0、libubsan0 等运行时检测库,以及 libmpc3、libisl19 等数学依赖,基本覆盖了从编译驱动到运行支持的完整链条;另有 1 个 sh 脚本,可用于简化批量安装流程。整个压缩包大约 20.05MB,体积适中,便于拷贝和分发。对需要复现旧版本编译环境或在隔离网络中搭建构建节点的读者来说,只要按依赖顺序依次安装,就能得到可用的 gcc 7,也能借此理清 gcc 各组件之间的依赖层级,减少版本错配带来的反复试错。目前已有 3205 人学习下载。
1. 为什么 Ubuntu 18.04 上还得手动备一份 gcc7 的离线包
在 ubuntu18.04 上重新装回 gcc7,最常发生在三种机器上:一是内网编译机,仓库不可达,apt 只能干看着;二是被同事“升级过一轮”的开发机,gcc 被切到 9 或 10,老代码一编译就报error: ‘auto_ptr’ is not a member of ‘std’;三是打包成镜像的 CI 节点,基础镜像被裁掉了一堆-dev包。这三种场景的共同点是:你手里必须有.deb实物,并且得会用 dpkg 把它按顺序喂进去。
这份ubuntu18.04gcc.zip就是干这个的——一整组针对 18.04 的 gcc-7 及其依赖包,解压后可以直接dpkg -i,不用联网。它适合两类人:被离线环境卡住、需要在半小时内把编译链恢复的运维和构建工程师;以及要给老项目锁死 gcc-7 版本、不希望全局编译器被别的东西改掉的开发者。下面把它拆开看。
2. 拆包看结构:gcc7 离线包里到底装了什么
拿到一个压缩包,第一步不是急着装,而是搞清楚里面有哪些 deb、它们互相依赖什么、版本后缀对不对得上本机。这一步做扎实,后面能少折腾两小时。
2.1 依赖链:gcc-7 从来不是单独一个 deb
很多人对gcc的直觉是“一个编译器就是一个包”,翻车就翻在这里。Ubuntu 把 gcc 拆得非常细:gcc-7本体只是个驱动程序(driver),负责解析命令行、拼参数、调用cc1/cc1plus做真正的前端处理,再调用as汇编、collect2和ld链接。所以它必然依赖binutils,也必然依赖与自己严格同版本的cpp-7、gcc-7-base、libgcc-7-dev。
再往上,写 C++ 的话还需要g++-7和libstdc++-7-dev;编译任何真实项目都离不开libc6-dev提供的头文件和启动文件(crt1.o、crti.o)。下表是我拆这类包时最关注的一份清单,按“缺了会怎样”排序:
| 包名 | 作用 | 缺了之后的表现 |
|---|---|---|
gcc-7-base | 版本元数据、license、公共文件 | 后续包 preinst 直接失败 |
cpp-7 | C 预处理器 | gcc-7: error trying to exec 'cc1' |
gcc-7 | 编译器驱动与 cc1 | 命令不存在 |
libgcc-7-dev | libgcc.a、crtbegin.o等 | 链接期cannot find crtbegin.o |
g++-7 | C++ 驱动与 cc1plus | 无法编译 .cpp |
libstdc++-7-dev | C++ 标准库头文件与静态库 | fatal error: iostream: No such file |
binutils | as / ld / ar | cannot find -lc、汇编失败 |
注意libgcc-7-dev和libstdc++-7-dev这两个包名里的-dev后缀,它们装的是编译期需要的东西;运行期只需要libgcc1和libstdc++6,而这两个在 18.04 基础系统里本来就有。搞混这一点,就会出现在编译机上装了一堆运行时包、却依然报头文件找不到的怪事。
2.2 版本号后缀是判断“能不能装”的第一证据
Ubuntu 的包版本号里藏着一个关键信息:~18.04这个后缀。它表示这个包是专门为 18.04 重新构建的,链接的是 18.04 的 glibc(2.27)。如果压缩包里混进了~20.04或者~16.04的包,装上去可能不报错,但运行期会在GLIBC_2.29 not found上炸掉,而且报错位置通常在业务代码里,极难定位。
解压之后先看一眼再动手:
# 解压到临时目录,不要直接在家目录里铺开 mkdir -p /tmp/gcc7-inspect && cd /tmp/gcc7-inspect unzip -q ~/Downloads/ubuntu18.04gcc.zip -d ./pkg # 列出所有 deb,先看文件名里的版本后缀 ls -1 ./pkg/*.deb | sed 's#.*/##' | sortunzip -q的-q是静默模式,包大时不会刷屏;-d ./pkg指定输出目录,避免把一堆 deb 撒在当前目录。文件名里一般形如gcc-7_7.5.0-3ubuntu1~18.04_amd64.deb,~18.04和amd64这两段就是你需要的判断依据。如果是arm64的包混进 x86 机器,dpkg 会直接拒绝,反倒好办;麻烦的是架构对但发行版不对的情况。
接着逐个读 control 字段,这一步比看文件名可靠得多,因为文件名可以改,control 里的Depends不会骗人:
# 逐个读取 control 关键字段:包名、版本、架构、依赖 for f in ./pkg/*.deb; do echo "== $(basename "$f")" dpkg-deb -f "$f" Package Version Architecture Depends donedpkg-deb -f是读 control 字段的专用开关,后面跟字段名即可,可以一次跟多个。Depends里会写成像cpp-7 (>= 7.5.0-3~), gcc-7-base (= 7.5.0-3ubuntu1~18.04), binutils (>= 2.26)这样的形式,注意那个=号——它表示版本必须完全相等,这也是为什么gcc-7和gcc-7-base必须来自同一个压缩包、同一批次,混用两个来源的包几乎必挂。
2.3 一次性把依赖缺口算出来,别用 dpkg 试错
新手最容易犯的错是抓一个 deb 就dpkg -i,报错了再装下一个,来回十几轮。dpkg 的依赖检查是“装的时候才查”,没有预演习惯的话,中间态会一直处于 half-configured,越弄越乱。
正确做法是先做一次静态比对:把包里所有Depends抽出来去重,再和本机已安装包列表求差集,缺什么一目了然。
cd /tmp/gcc7-inspect # 1) 抽出所有 deb 声明的依赖,去掉版本约束和可选分支,去重排序 for f in ./pkg/*.deb; do dpkg-deb -f "$f" Depends; done \ | tr ',' '\n' \ | sed 's/(.*//; s/|.*//' \ | tr -d ' ' \ | grep -v '^$' \ | sort -u > need.txt # 2) 导出本机已安装包名 dpkg-query -W -f='${Package}\n' | sort -u > have.txt # 3) 求差集:need 里有、have 里没有的,就是缺口 comm -23 need.txt have.txt三段管道的作用分别是:tr ',' '\n'把逗号分隔的依赖拆成一行一个;sed 's/(.*//'砍掉括号里的版本约束,s/|.*//处理“或”关系的备选分支(比如default-libc-dev | libc6-dev取前者即可);tr -d ' '清掉残留空格。最后comm -23需要两个输入都已排序,所以前面必须带sort -u,否则结果会是错的——这是 comm 最常见的误用。
输出为空,说明本机基础依赖齐全,直接装包即可;输出里出现libisl15、libmpc3、libmpfr6、zlib1g这类数学库和压缩库,说明压缩包里应该也带了,检查一下是不是漏解压了某个子目录。如果缺失的是binutils或libc6-dev这种基础包,优先从本机 ISO 或本地源补,而不是从别处拷一个高版本的过来——版本一高,libc6会被连带升级,整机风险陡增。
另外可以先用--no-act让 dpkg 走一遍解析流程但不落盘:
# 空跑:只解析不写入,用来提前看 dpkg 会抱怨什么 sudo dpkg --no-act -i ./pkg/*.deb 2>&1 | head -40--no-act(等价--dry-run)只打印“将会做什么”,不会真的解包和写文件。它的依赖检查不如真实安装严格,所以别把它当权威结论,但用来提前发现“包名拼错”“架构不符”这类低级问题足够了。
3. 安装顺序:dpkg -i 一次过还是逐个来
搞清楚依赖之后,安装本身其实只有一条命令。但这条命令的写法有讲究,写错了就会得到一堆dependency problems prevent configuration。
3.1 为什么顺序重要:preinst 和配置时机的差别
dpkg 装一个包分两步:先unpack(解包到文件系统),再configure(跑preinst/postinst脚本、写状态库)。依赖检查发生在 configure 阶段。也就是说,如果你一个一个装,装gcc-7的时候cpp-7还没 configure,dpkg 就会把这个包留在unpacked状态,等你装完cpp-7还得回头dpkg --configure gcc-7,非常啰嗦。
而dpkg -i a.deb b.deb c.deb这种一次传多个文件的写法,dpkg 会在同一批次内先把所有包 unpack,再按依赖关系排序 configure。这就是为什么“一次过”反而比“一个一个来”稳。
正确的一次性写法:
cd /tmp/gcc7-inspect/pkg # 一次性把所有 deb 交给 dpkg,让它自己排 configure 顺序 sudo dpkg -i ./*.deb./*.deb由 shell 展开成完整文件列表,dpkg 拿到的是全部包。注意这里不能用-R(递归)去指向上级目录,-R只处理目录里直接存在的 deb,行为和通配符并不等价,混用容易漏包。
如果包数量很多、想显式控制“基础包先上”,可以分批但保证每批内部自洽:
# 第一批:纯数据与基础库,无外部依赖 sudo dpkg -i gcc-7-base_*.deb libisl15_*.deb libmpc3_*.deb libmpfr6_*.deb # 第二批:预处理与后端 sudo dpkg -i cpp-7_*.deb binutils_*.deb libgcc-7-dev_*.deb # 第三批:驱动与 C++ 前端 sudo dpkg -i gcc-7_*.deb g++-7_*.deb libstdc++-7-dev_*.deb分三批的依据是依赖方向:第一批没有任何对包内其他成员的依赖,第二批只依赖第一批,第三批依赖前两批。这样即使中途某批失败,系统也不会卡在互相等待的中间态,重试成本低。
还有一种保底写法是加--force-depends:
# 仅作为最后手段:强行忽略依赖检查 sudo dpkg -i --force-depends ./gcc-7_*.deb sudo dpkg --configure -a--force-depends会让 dpkg 跳过依赖校验强行装,代价是可能留下一个“装上了但跑不起来”的包。我的建议是:只在确认缺失的那个依赖对当前用途完全无影响时才用(比如缺的是一个文档包),编译器本身的依赖缺口一律不许强装,否则后面编译报的错会离真正原因十万八千里。
3.2 离线机器上 apt 帮不了你,别指望 -f install
网上一半的教程会让你在报错后跑sudo apt-get -f install。这条命令在联网机器上确实好用,它会自动补齐缺失依赖。但在内网离线机上,apt 找不到源,只会返回一堆Failed to fetch,然后把你的包状态搅得更乱。
离线场景的替代方案是dpkg --configure -a加手工补包:
# 查看当前处于异常状态的包(unpacked / half-configured) sudo dpkg --audit # 尝试把所有未完成配置的包配置完 sudo dpkg --configure -adpkg --audit是排查“上次装到一半断了”的第一条命令,它会列出所有处于iU(unpacked)、iF(half-configured)、iH(half-installed)状态的包。--configure -a会按依赖顺序重跑这些包的 postinst。跑完之后再dpkg --audit一次,输出为空才算干净。
3.3 装完必做的三条验证
装完不验证,等于没装。三条命令分别从包状态、可执行文件、头文件三个层面确认:
# 1) 包状态:第二列应为 ii(已安装且已配置) dpkg -l | grep -E 'gcc-7|cpp-7|g\+\+-7|libgcc-7|libstdc\+\+-7' # 2) 可执行文件是否就位,版本是否符合预期 /usr/bin/gcc-7 --version | head -1 /usr/bin/g++-7 --version | head -1 # 3) 这个可执行文件到底属于哪个包,防止被别的包“借位” dpkg -S /usr/bin/gcc-7第一条里dpkg -l的第二列是状态码,ii才是正常,iU表示只解包没配置,rc表示已删除但残留配置。第二条要确认版本号是7.5.0这一支,如果显示的是 7.4 或者干脆找不到文件,说明压缩包里的包没被正确解压或装到了别的前缀。第三条dpkg -S是查文件归属,输出应该指向gcc-7,如果指向gcc-9,说明你机器上存在路径冲突,需要进一步查。
再补一条更实用的检查——直接编译一个最小程序,把预处理、编译、汇编、链接四步全走一遍:
cat > /tmp/probe.c <<'EOF' #include <stdio.h> int main(void) { printf("%s\n", __VERSION__); return 0; } EOF gcc-7 /tmp/probe.c -o /tmp/probe && /tmp/probe能打印出版本串,说明cc1、as、collect2、ld、libc启动文件这条链路全部打通。这一步比看--version有价值得多,--version只证明驱动在,不证明后端在。
4. 多版本共存:把 gcc7 挂进 alternatives 并按项目切换
装好了gcc-7,但敲gcc出来的还是别的版本,这是最高频的困惑。原因很简单:/usr/bin/gcc是个软链接,由update-alternatives管理,你装了gcc-7不等于系统会用它。
4.1 alternatives 的机制与优先级数字怎么定
update-alternatives维护的是/etc/alternatives/下的一组二级链接,结构是:/usr/bin/gcc→/etc/alternatives/gcc→/usr/bin/gcc-7。注册时给的那个优先级数字,只在auto模式下决定默认选中谁——数字大的赢。它不是“版本高低”的意思,纯粹是个权重。
所以优先级怎么定,取决于你的意图。如果这台机器是给老项目用的,希望默认就是 7,那就给 70 以上的数字并把别的版本压下去;如果只是临时备着,给个小数字、需要时手动--config切换即可。
slavery(从链接)是这套机制里最省事的部分:注册gcc的时候把g++、gcov一起挂上,切换时三个一起变,不会出现“gcc 是 7、g++ 还是 9”这种 ABI 混搭的灾难。
4.2 注册并切换
# 注册 gcc-7,同时把 g++、gcov 作为从链接绑定 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 70 \ --slave /usr/bin/g++ g++ /usr/bin/g++-7 \ --slave /usr/bin/gcov gcov /usr/bin/gcov-7 # 查看当前候选与模式 update-alternatives --query gcc # 交互式切换 sudo update-alternatives --config gcc--install后面依次是:主链接路径、alternatives 名字、实际文件路径、优先级。名字(这里是gcc)是这套链接的逻辑标识,后面所有操作都靠它。--slave的三个参数是:从链接路径、从链接名、从文件路径——从链接不能独立切换,只能跟着主链接走,这正是我们要的效果。
--query会输出Status、Best、Value三个关键行。Status: auto表示按优先级自动选,manual表示被--config锁定过。如果你的机器上Status是manual且Value指向 gcc-9,那--install加进去也不会生效,必须先--config切回来。
# 确认最终指向 readlink -f /usr/bin/gcc readlink -f /usr/bin/g++readlink -f会一路跟到底,输出真实文件路径。两条命令的输出里版本号必须一致,这是排查 ABI 混用的第一道关卡。
4.3 不改全局:在项目层指定编译器
我个人的习惯是不轻易动全局 alternatives。一台机器上往往同时有好几个项目,全局切一次,别人的构建就挂了。更稳的做法是在项目里指定编译器,让版本跟着代码走。
Makefile 里就两行:
# 项目级锁定编译器,不依赖系统默认 CC := gcc-7 CXX := g++-7 CXXFLAGS += -std=c++14 -D_GLIBCXX_USE_CXX11_ABI=0_GLIBCXX_USE_CXX11_ABI=0这个宏值得单独说一句。gcc5 之后 libstdc++ 引入了新的std::stringABI(cxx11 ABI),而 gcc-7 默认开启它。如果你的项目要链接第三方预编译好的.so(尤其是那些用老编译器构建的),就会出现undefined reference to std::__cxx11::basic_string...这种链接错误。这时候把 ABI 开关关掉,比去找第三方要新版本快得多。
CMake 项目则在配置阶段传变量,注意必须是首次配置时传,改完要清空 build 目录,否则 CMakeCache 里存着旧的编译器路径:
# 首次配置时指定,之后改需先删 build 目录 cmake -S . -B build \ -DCMAKE_C_COMPILER=/usr/bin/gcc-7 \ -DCMAKE_CXX_COMPILER=/usr/bin/g++-7 cmake --build build -j"$(nproc)"-S和-B是 CMake 3.13 之后的新写法,分别指源码目录和构建目录,比进目录再cmake ..清晰。-j"$(nproc)"按 CPU 核数并行编译,老项目头文件多的时候能省不少时间。
4.4 验证:从版本串到 ABI 符号
光看--version不够,得让编译器自己把版本、标准、ABI 都吐出来:
cat > /tmp/abi.cpp <<'EOF' #include <iostream> #include <string> int main() { std::string s = "probe"; std::cout << "compiler: " << __VERSION__ << "\n" << "std: " << __cplusplus << "\n" << "string size: " << sizeof(s) << "\n"; return 0; } EOF g++-7 -std=c++17 /tmp/abi.cpp -o /tmp/abi && /tmp/abi重点看string size。在开启 cxx11 ABI 的情况下,64 位下std::string通常是 32 字节;关掉 ABI 后是 8 字节(本质上是个指针)。这个数字能一秒判断当前编译单元用的是哪套 ABI,比翻编译日志快得多。
再确认运行期库的符号版本跟编译期对得上:
# 看系统 libstdc++ 支持到哪个 CXXABI 版本 strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep -E '^CXXABI_1\.3\.[0-9]+$' | sort -V | tail -3strings从二进制里抽可打印字符串,grep筛出 ABI 版本标记,sort -V按版本号语义排序(不能用普通sort,否则1.3.9会排在1.3.11后面)。gcc-7 编译出来的代码最高会用到CXXABI_1.3.11附近,如果系统库只到1.3.9,那就得把libstdc++6一起升级,否则程序一启动就报version 'CXXABI_1.3.11' not found。
5. 避坑与排查:dpkg 依赖断裂、ABI 混用、alternatives 抢位的五个现场
这一章是我这几年在离线机上反复踩过的坑,按出现频率从高到低排。每条都按“看到什么 → 为什么 → 怎么办”写,遇到时直接对号入座。
5.1 现象:dependency problems prevent configuration of gcc-7
报错里还会跟着一行Package cpp-7 is not configured yet.,然后整个安装就停在那里。
原因是 dpkg 是逐个 configure 的,当你把 deb 一个一个装、或者压缩包解压不完整导致少了某个成员时,gcc-7的 configure 阶段会检查依赖是否已经处于installed状态,没满足就拒绝配置,包被留在iU。
解决办法分两步。先确认压缩包成员是否齐全:ls ./pkg/*.deb | wc -l,再跟 2.1 节的清单对一遍。齐全的话,用一次性安装覆盖重来:sudo dpkg -i ./pkg/*.deb,dpkg 会重新按依赖排序配置。如果还是不行,sudo dpkg --configure -a兜底。不要用apt-get -f install,离线机上它只会让状态更乱。
5.2 现象:装完敲gcc --version还是旧版本,或者干脆command not found
--version显示旧版本,说明/usr/bin/gcc的软链接没指向 gcc-7,alternatives 没注册或注册了但处于manual锁定态。command not found则更严重,通常是 alternatives 被删过、/etc/alternatives/gcc成了断链。
先查状态:update-alternatives --query gcc,看Value和Status两行。Status: manual且Value指向旧版本,就sudo update-alternatives --config gcc手动选。如果是断链,先sudo update-alternatives --remove gcc /usr/bin/gcc-7清掉坏记录再重新--install。还有一种极少见的情况:/usr/bin/gcc被某个自行编译安装的 gcc 覆盖成了实体文件(不是软链接),这时候 alternatives 完全失效,得先手动删掉那个实体文件再重新注册。
5.3 现象:链接期报undefined reference to std::__cxx11::basic_string<...>
编译阶段一切正常,一到链接就炸,报错里全是std::__cxx11::开头的长符号。
这是典型的 cxx11 ABI 不匹配:你的代码用 gcc-7 默认 ABI 编译,但链接的第三方.so是用老 ABI(对应_GLIBCXX_USE_CXX11_ABI=0)构建的,两边的std::string内存布局完全不同,符号名也不同,自然对不上。
解决方向有两个:能拿到第三方源码就重编一遍;拿不到就在编译选项里加-D_GLIBCXX_USE_CXX11_ABI=0,让整个项目退回老 ABI。加之前先确认项目里所有翻译单元都加了,只加一半会得到更难查的运行时崩溃——因为链接能过,但对象布局不一致。
5.4 现象:/usr/bin/ld: cannot find -lstdc++或cannot find crtbegin.o
这两种报错指向同一个根因:只装了gcc-7,没装g++-7和libgcc-7-dev。crtbegin.o属于libgcc-7-dev,libstdc++.so的开发符号链接属于libstdc++-7-dev。
用dpkg -L libgcc-7-dev | grep crtbegin确认文件在不在,dpkg -L libstdc++-7-dev | grep 'libstdc++.so'确认开发链接在不在。如果包状态是ii但文件缺失,说明安装过程被中断过,sudo dpkg --configure -a或sudo dpkg -i --force-overwrite重装该包。另外提醒一点:libstdc++6(运行库)和libstdc++-7-dev(开发库)是两个包,前者在基础系统里就有,后者必须从压缩包里装,别看到libstdc++前缀就以为已经装了。
5.5 现象:dpkg was interrupted, you must manually run 'dpkg --configure -a'
这条提示一般出现在你上次装到一半按了 Ctrl-C、或者 SSH 断线、或者机器恰好断电之后。此时 dpkg 的锁文件和状态库都处于不一致状态,任何dpkg -i或apt操作都会被拒绝。
按提示走就行,但顺序要对:先sudo rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock清锁(只有在确认没有别的 dpkg 进程在跑时才这么做,用ps aux | grep dpkg确认),再sudo dpkg --configure -a,最后sudo dpkg --audit确认没有残留异常包。清锁这一步别乱来——如果真有另一个 apt 进程在后台跑,删锁会导致状态库损坏,修复成本远高于等它跑完。
6. 进阶:把这份包做成可复用的内网安装脚本
单次装好只是解决一台机器。真正省时间的是把它固化成一个脚本,扔到内网任何一台 18.04 上都能跑,跑完自己报告结果。脚本我一般写成四段:环境校验、解压清点、安装、验证输出。
#!/usr/bin/env bash # install-gcc7.sh —— 内网 18.04 机器上的 gcc-7 离线安装 set -euo pipefail ZIP="${1:?用法: ./install-gcc7.sh /path/to/ubuntu18.04gcc.zip}" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT # 1) 只允许 18.04,版本不对直接退出,避免装出 ABI 烂摊子 . /etc/os-release [ "$VERSION_ID" = "18.04" ] || { echo "仅支持 18.04,当前 $VERSION_ID"; exit 1; } # 2) 解压并清点,数量少于预期就中止 unzip -q "$ZIP" -d "$WORK/pkg" COUNT=$(find "$WORK/pkg" -name '*.deb' | wc -l) echo "解出 deb 包: $COUNT 个" [ "$COUNT" -ge 6 ] || { echo "包数量异常,检查压缩包完整性"; exit 1; } # 3) 一次性安装,失败则打印未配置的包 dpkg -i "$WORK"/pkg/*.deb || { dpkg --audit; exit 1; } # 4) 注册 alternatives 并输出验证结果 update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 70 \ --slave /usr/bin/g++ g++ /usr/bin/g++-7 >/dev/null printf '#include <stdio.h>\nint main(void){puts(__VERSION__);return 0;}\n' > "$WORK/p.c" gcc-7 "$WORK/p.c" -o "$WORK/p" && "$WORK/p"几个关键点值得说明。set -euo pipefail让脚本遇到未定义变量、管道中任一段失败、任意命令非零退出时立刻中断,避免半途失败还继续往下跑。trap ... EXIT保证无论正常结束还是异常退出都清理临时目录,否则/tmp里会堆一堆解压残留。${1:?...}是参数必填语法,不给参数时直接打印用法并退出,比跑到一半才发现路径是空的好。最后那句printf生成探针程序并编译运行,是整个脚本的“验收环节”——前面的命令都可能返回 0,但只有真的编译出可执行文件、跑起来打印出版本串,才证明工具链是通的。
脚本之外,我还会在同目录留一份MANIFEST.txt,记录这次装的每个包的包名和完整版本号:
# 生成清单,出问题时能一秒对上是哪个包惹的祸 dpkg-query -W -f='${Package} ${Version} ${Status}\n' \ 'gcc-7' 'g++-7' 'cpp-7' 'gcc-7-base' 'libgcc-7-dev' 'libstdc++-7-dev' \ > MANIFEST.txt为什么非要这个清单?因为半年后你回来排查一台机器,--version只能告诉你 7.5.0,告诉不了你当初装的是7.5.0-3ubuntu1~18.04还是某个自己重编的版本;而版本号差一位,依赖的gcc-7-base就可能对不上,重装时又是一轮鸡同鸭讲。有了清单,直接照着版本号去压缩包里挑对应的 deb,几分钟搞定。
最后提一个容易被忽略的验证技巧:装完之后用dpkg -S反查所有 gcc 相关可执行文件的归属,把结果跟清单对照。如果发现某个文件属于一个意料之外的包(比如cc1来自gcc-9而不是cpp-7的gcc-7),说明系统里存在多套工具链交叉污染,这种污染在编译大型项目时会以各种离奇的方式暴露——比如某个.o文件明明编译成功了,链接时却报一堆莫名其妙的未定义符号。早发现,早清干净。
我以前图快,装完只看一眼gcc-7 --version就去编译业务代码,结果在一个混合了 18.04 和 20.04 包的内网镜像上栽了大跟头:编译期全过,运行期GLIBCXX_3.4.26 not found,查了整整一个下午才定位到是 base 镜像里混进了一个高版本的libstdc++6。从那以后,我每次装完这套离线包都强制走一遍“清点 → 比对 → 探针编译 → 生成 MANIFEST”这四步,一步都不跳。希望帮到你。
本文还有配套的精品资源,点击获取