简介:这份压缩包面向需要在无网络或网络不稳定环境中部署编译工具链的 Linux 运维与开发人员,围绕 GCC 离线安装提供成套 RPM 组件和自动化执行脚本,帮助避开在线源受限、依赖缺失等常见阻碍。包内共 12 个文件,以 11 个 rpm 依赖包和 1 个 sh 安装脚本为主,压缩包约 24.09MB;RPM 覆盖编译器、C++ 支持、头文件及基础库等组件,脚本用于串联安装流程。已有 558 人学习下载,适合需要在内网服务器、远程主机或实验环境中快速补齐 GCC 编译环境的读者参考。通过这份材料,读者可获得较完整的离线安装包组合与脚本化执行思路,减少逐项查找依赖、手动处理安装顺序的时间,也便于结合系统版本核对组件匹配情况并完成环境验证。
1. 一台断网机器和一个必须升级的 gcc
内网编译机上最常见的一幕:系统自带 gcc 4.8,业务代码里一个std::filesystem就编不过,想升版本却发现yum install根本连不上源。gcc 离线安装真正难的地方从来不是./configure && make,而是它背后那条依赖链——gmp、mpfr、mpc、isl 四个数学库加上一堆系统开发包,少一个就在 configure 阶段停下,而且报错信息往往只提一个包名,容易让人误以为只缺这一个。这份资源把 gcc 源码、四个依赖库源码和一个编排脚本打在一起,配合一台有网的机器做介质准备,就能在完全断网的环境里走完整个构建。它适合内网编译机、构建环境需要固定版本复现的团队,以及镜像站不可达的机器。下面按依赖拆解、介质整理、脚本执行、排错、验证的顺序往下走。
2. 依赖链拆解:gmp、mpfr、mpc、isl 各自管什么
很多人第一次做 gcc 离线安装,会下意识把 gcc 源码包拷过去直接 configure,然后在第三分钟撞上Building GCC requires GMP 4.2+, MPFR 2.3.1+ and MPC 0.8.0+这行提示。这不是 gcc 故意为难人,而是它的中间表示和常量折叠过程需要在编译期做任意精度的整数与浮点运算,编译器本身就是一个重度数值计算程序。所以理清这四个库谁依赖谁、版本怎么咬合,比背十个 configure 参数有用得多。
2.1 从 configure 报错反推依赖树
gcc 顶层 configure 会依次探测 gmp、mpfr、mpc,任何一个不满足下限就直接退出,而且只报第一个失败的检查项。这就是为什么有人补了 gmp 再跑一次又报 mpfr,来来回回折腾四五轮。做法很简单:一次性把三个库都准备好,别指望 configure 帮你列清单。
依赖关系本身是一条链:gmp 是底层的多精度整数库,mpfr 建立在 gmp 之上做多精度浮点,mpc 又建立在 gmp 与 mpfr 之上做复数运算。所以构建顺序必须是 gmp → mpfr → mpc,反过来一个都编不过。isl 是另一条支线,服务于 Graphite 循环优化框架,它自身依赖 gmp,但和 mpfr、mpc 没有关系,可以并行准备。
gcc 支持两种找到依赖的方式:一是把依赖装到独立前缀,用--with-gmp=/opt/deps --with-mpfr=/opt/deps --with-mpc=/opt/deps指过去;二是把依赖源码解压到 gcc 源码树里,目录名分别是gmp、mpfr、mpc、isl(必须去掉版本号),gcc 的顶层 make 会自动进去构建并静态链接。离线场景我更倾向第二种,原因在于它把依赖的构建完全收进 gcc 的构建流程,不需要预先编译一套依赖再维护路径,出问题的面小了一圈。
| 方式 | 适用场景 | 代价 |
|---|---|---|
| 解压进源码树 | 单机离线、只为一个 gcc 服务 | 依赖不能复用给其他项目 |
独立前缀 +--with-* | 多版本 gcc 共用同一套依赖 | 需要先单独构建依赖并管理路径 |
提示:解压进源码树时目录名写错成
gmp-6.2.1是最常见的低级错误,gcc 会当作没看见,然后继续报缺 gmp。
2.2 四个数学库的版本咬合关系
版本不是越新越好。gcc 的 configure 对 isl 有明确的 API 检查,装了一个过新的 isl 有时会让 Graphite 相关代码编译失败;反过来,版本低于 gcc 要求又会直接被拒。稳妥策略是以 gcc 源码包自带的contrib/download_prerequisites脚本里写死的版本为准——那个脚本里清清楚楚列着这四行包名和版本号,它就是这个 gcc 版本官方验证过的组合。离线环境虽然跑不了这个脚本(后面会讲它为什么要联网),但把脚本当版本清单读是完全没问题的。
具体做法:打开contrib/download_prerequisites,找到类似gmp=gmp-6.2.1.tar.bz2、mpfr=mpfr-4.1.0.tar.bz2、mpc=mpc-1.0.3.tar.gz、isl=isl-0.18.tar.bz2的赋值行,照抄文件名去准备介质即可。如果手上没有对应版本的源码包,退一步选择版本号略高的同主版本,别跨大版本。
还要注意一点:gcc 内置构建依赖时默认生成静态库,所以最终产物不依赖这些库的 .so,安装完 gcc 之后就算把依赖源码目录删掉也不影响 gcc 运行。这点在磁盘紧张的机器上很有用。
2.3 系统层的隐形依赖:flex、bison、zlib-devel 与 m4
四个数学库只是明面上的依赖。真正让离线安装翻车的常常是系统层的开发包,它们不在 configure 的检查列表里,而是在 make 跑到一半时以一个莫名其妙的错误停下来。
| 包 | 何时需要 | 缺了会怎样 |
|---|---|---|
| flex / bison | 修改了.l/.y源文件,或源码包未预生成解析器 | make 报flex: command not found |
| m4 | 构建 gmp(gmp 的 configure 依赖 m4 展开宏) | gmp configure 阶段失败,报 m4 相关错误 |
| zlib-devel | 使用--with-system-zlib | configure 报找不到 zlib.h |
| texinfo | 使用makeinfo生成文档(默认构建会尝试) | make 报makeinfo: command not found,可忽略但干扰判断 |
| gperf | 部分目标平台生成完美哈希时使用 | 少数场景下 make 失败 |
| make / tar / xz / bzip2 | 始终需要 | 解压或构建直接进行不下去 |
离线环境里这些包同样是 rpm 或 deb,需要在有网机器上用yumdownloader --resolve或apt-get download把依赖一并下载。这里有个血泪经验:yumdownloader默认只下指定包,不加--resolve就不会把依赖带上,等到装的时候才发现缺了一串。
至于--with-system-zlib,我的建议是离线环境别开。开了就得额外准备 zlib 开发包,不开则 gcc 用自带的 zlib 副本,多花几分钟编译,换来确定性。
3. 离线介质整理:把源码包摆成脚本认识的样子
介质准备这一步做扎实,后面执行脚本基本就是一条命令的事;这一步糊弄,后面每跑一次都要重新猜错误。核心思路是把「源码」「依赖包」「构建产物」三层彻底分开,让脚本只依赖目录约定,不依赖你记性。
3.1 目录布局:源码、依赖、产物三层分开
我一般把整个目录组织成这样,脚本里所有路径都由根目录推导,拷到任何机器上都能直接跑:
gcc-offline/ ├── pkgs/ # 原始压缩包,只读,不改动 │ ├── gcc-11.3.0.tar.xz │ ├── gmp-6.2.1.tar.bz2 │ ├── mpfr-4.1.0.tar.bz2 │ ├── mpc-1.0.3.tar.gz │ └── isl-0.18.tar.bz2 ├── build/ # out-of-tree 构建目录,可整目录删除 ├── install.sh # 编排脚本 └── gcc-11.3.0/ # 解压后的源码树(脚本自动生成)关键点是构建目录独立于源码目录。gcc 的老文档里确实有在源码树里直接 configure 的写法,但那样一旦构建失败想重来,就得make distclean或者重新解压,而make distclean在依赖已经内嵌的情况下会把解压好的 gmp 目录里的中间文件也删掉,下一次构建又得重新解压一遍,很浪费时间。用mkdir build && cd build && ../gcc-x.y.z/configure ...的方式,重建时rm -rf build就干净了。
源码树里内嵌依赖的目录名必须精确:gmp、mpfr、mpc、isl,全小写,不带版本后缀。这一点前面提过,但实际翻车率太高,值得再强调一次。
3.2 绕开 download_prerequisites 的联网动作
gcc 源码包里的contrib/download_prerequisites是官方给的依赖获取脚本,它做的事情其实很朴素:用wget或curl从 GNU 镜像下载四个压缩包,然后解压、改名、删掉压缩包。整个过程没有任何魔法,但它默认一定会发起网络请求。
离线机器上直接跑它的后果是卡在下载阶段,超时后报一个 curl 错误,或者在某些版本里更糟——用set -e的情况下直接退出,用交互式 shell 的情况下可能留下半截文件。更隐蔽的情况是它检测到目录已经存在就跳过下载,但只做了部分解压,留下一个空目录,gcc configure 检查目录存在就以为依赖齐了,于是构建到一半才炸。
等价的手工操作就是三步:解压、改名、确认目录里有configure文件。脚本里写成函数,重复执行也不会出问题(先判断目录是否已存在就返回)。
# 把 pkgs/ 下的依赖包解压进 gcc 源码树,目录名去掉版本号 # 已存在则跳过,保证脚本可以重复执行 stage_dep() { local tarball="$1" # 压缩包文件名,例如 gmp-6.2.1.tar.bz2 local dirname="$2" # 目标目录名,例如 gmp local srcdir="$3" # gcc 源码树根目录 if [ -d "${srcdir}/${dirname}" ] && [ -f "${srcdir}/${dirname}/configure" ]; then echo "[skip] ${dirname} 已就绪" return 0 fi # ${tarball%.tar.*} 把 gmp-6.2.1.tar.bz2 截成 gmp-6.2.1 local base="${tarball%.tar.*}" tar -xf "${tarball}" -C "${srcdir}" || return 1 mv "${srcdir}/${base}" "${srcdir}/${dirname}" || return 1 echo "[ok] ${dirname} <- ${base}" }逻辑上讲清楚两处:${tarball%.tar.*}是 bash 的参数展开,从右往左删掉最短匹配,.tar.bz2、.tar.gz、.tar.xz三种后缀都能正确截断;用[ -f "${srcdir}/${dirname}/configure" ]而不是只判断目录存在,是为了挡住「目录建了但内容是空的」这种半截状态,这正是 download_prerequisites 中途失败会留下的情况。
3.3 介质完整性校验与解压
离线介质的另一个隐患是传输损坏。U 盘拷一半拔了、内网文件服务器断点续传没对齐,都会产生一个能解开但内容不完整的 tar 包,解压时报unexpected end of file。脚本开头加一段校验能把这类问题挡在最前面。
# 逐个校验压缩包能否被 tar 正常读取 # 只做 list 不落盘,速度快,不污染目标目录 check_tarballs() { local pkgdir="$1" local rc=0 for f in "${pkgdir}"/*.tar.*; do if ! tar -tf "${f}" >/dev/null 2>&1; then echo "[bad] ${f} 校验失败,重新拷贝该文件" rc=1 fi done return ${rc} }tar -tf会把整个归档遍历一遍做列表输出,重定向到/dev/null。它比md5sum更实用,因为你不一定拿得到官方校验值,但 tar 自己的结构校验足以发现截断和损坏。代价是要完整读一遍文件,几十兆的包无所谓。
注意:校验函数返回非零时一定要让主脚本退出。见过太多脚本把校验结果打印出来却继续往下跑,最后在 make 阶段花两小时才发现源码包是坏的。
4. 构建脚本逐段拆解:configure 参数与并行度的取值
脚本本身不复杂,难的是每个参数背后的取舍。下面这份骨架可以直接抄,注意把版本变量改成你介质里实际的目录名。
4.1 脚本骨架与前置自检
前置自检的目的不是优雅,而是把失败点尽量前移。gcc 完整构建动辄四十分钟到两小时,等 make 跑到一半才发现缺 flex,成本太高。
#!/usr/bin/env bash set -euo pipefail # 任一命令失败即退出,未定义变量报错,管道错误不吞 GCC_VER="gcc-11.3.0" # 与 pkgs/ 里的源码包名保持一致 PREFIX="/opt/${GCC_VER}" # 安装前缀,刻意不装到 /usr ROOT="$(cd "$(dirname "$0")" && pwd)" SRC="${ROOT}/${GCC_VER}" BUILD="${ROOT}/build" JOBS="${JOBS:-$(nproc)}" # 允许外部覆盖,默认用满核数 # 1) 基础命令自检 for c in make gcc g++ tar xz bzip2; do command -v "$c" >/dev/null 2>&1 || { echo "缺少命令: $c"; exit 1; } done # 2) 宿主 gcc 版本自检:低于 4.8 无法构建现代 gcc host_major=$(gcc -dumpversion | cut -d. -f1) [ "${host_major}" -ge 4 ] || { echo "宿主 gcc 过旧: $(gcc -dumpversion)"; exit 1; } # 3) 磁盘空间自检,构建 + 安装至少预留 15G avail=$(df -Pk "${ROOT}" | awk 'NR==2{print int($4/1024/1024)}') [ "${avail}" -ge 15 ] || { echo "磁盘不足,剩余 ${avail}G"; exit 1; }第一段set -euo pipefail是整份脚本的安全带,尤其是pipefail——没有它,gcc -dumpversion | cut -d. -f1这类管道里前半段失败会被后半段的成功掩盖。第三段的磁盘检查用df -Pk拿 KB 数再换算成 GB,-P保证输出格式固定(避免长设备名换行导致awk取错行)。
4.2 configure 参数逐条解释
configure 是整个流程里最需要想清楚的一步,参数错了不是报错,而是编出来一个你不想要的东西,装完才发现没有 C++ 或者跑不了 32 位程序。
mkdir -p "${BUILD}" && cd "${BUILD}" "${SRC}/configure" \ --prefix="${PREFIX}" \ --enable-languages=c,c++ \ --disable-multilib \ --disable-bootstrap \ --enable-checking=release \ --with-system-zlib=no--prefix指定安装位置。这里刻意不用/usr,因为离线机器上往往已经有发行版自带的 gcc,覆盖安装轻则包管理器状态错乱,重则系统里的某些工具链依赖被换掉导致别的东西编不过。装到/opt/gcc-x.y.z之后再通过 PATH 或 alternatives 切换,是可控得多的做法。
--enable-languages=c,c++只编 C 和 C++ 前端。全量构建包含 Fortran、Ada、Go 等前端,编译时间能翻两三倍,离线编译机通常用不上。如果真的需要 Fortran,把它加进逗号列表即可,代价是多花二十分钟。
--disable-multilib关闭 32 位运行时支持。x86_64 上默认开启 multilib,构建过程中需要 32 位的 glibc 开发包(glibc-devel.i686之类),这在离线环境里又是一堆要下载的 rpm。除非你明确要编 32 位程序,否则关掉。
--disable-bootstrap关闭三轮自举。gcc 默认会先用宿主编译器编出 stage1,再用 stage1 编 stage2,再验证 stage2 和 stage3 的产物是否一致。这套流程是为了验证 gcc 自身的正确性,对发行版构建很有意义,对「我只想要一个新版 gcc」的运维场景则是纯粹的三倍时间。关掉后只编一轮,风险是新编译器本身的 bug 不会被自举过程暴露——但用官方发布的稳定版本源码,这个风险可以接受。
--enable-checking=release关闭内部断言检查,生成的编译器运行更快。默认值在某些配置下是yes,会让 cc1plus 慢一截。
--with-system-zlib=no明确拒绝使用系统 zlib,改用源码树内的副本,省掉一个离线依赖。
| 参数 | 推荐值 | 主要影响 |
|---|---|---|
--prefix | /opt/gcc-<版本> | 避免覆盖系统工具链 |
--enable-languages | c,c++ | 决定前端数量与构建时长 |
--disable-multilib | 开启 | 免去 32 位开发包依赖 |
--disable-bootstrap | 开启 | 构建时间约为默认的三分之一 |
--enable-checking | release | 编译器运行速度 |
--with-system-zlib | no | 减少一个离线依赖 |
4.3 make 并行度、内存与 install 阶段
make -j的取值是离线编译最容易翻车的地方。gcc 的单个 cc1plus 进程在处理大型翻译单元时峰值内存能到 1.5G 以上,-j$(nproc)在 16 核 16G 的机器上会直接把内存打满,然后看到virtual memory exhausted: Cannot allocate memory或者进程被 OOM killer 干掉,make 报一个没有任何上下文的Killed。
我的经验值是按内存算而不是按核数算:JOBS = min(核数, 内存GB / 2)。32G 内存的 16 核机器,取-j16一般安全;16G 内存的 16 核机器,老老实实-j8。构建时间长一点,总比跑到 80% 崩掉重来强。
# 并行度按内存收敛,避免 OOM mem_gb=$(awk '/MemTotal/{print int($2/1024/1024)}' /proc/meminfo) by_mem=$(( mem_gb / 2 )) [ "${by_mem}" -lt 1 ] && by_mem=1 [ "${JOBS}" -gt "${by_mem}" ] && JOBS="${by_mem}" echo "使用并行度: ${JOBS}(内存 ${mem_gb}G)" make -j"${JOBS}" make install-strip # 安装并剥离调试符号,省一半磁盘make install-strip相比make install会剥离二进制里的调试符号,安装体积能小一半左右,对 gcc 这种二进制巨大的东西很划算。代价是出问题时没法用 gdb 深入调试编译器本身——但一般也用不上。
还有一个细节:make install阶段不要并行。某些版本的安装规则里存在目录创建和文件拷贝的顺序依赖,-j下偶发cannot create directory的错误,而且这种错误每次位置都不一样,很难查。安装阶段老老实实单线程,几秒钟的事。
5. 五个高频翻车点:从 configure 到 install 的排查清单
下面五条都是我在离线环境里真实遇到过的,按发生频率排序。每条按「现象 → 原因 → 解决」写,方便对照。
5.1 configure 报缺 gmp,但源码明明已经解压
现象:configure: error: Building GCC requires GMP 4.2+ ...,而ls一下源码树,gmp目录就在那儿。
原因:目录名对了但内容不对。多半是解压时用了tar -xf gmp-6.2.1.tar.bz2之后直接mv gmp-6.2.1 gmp之前被打断,或者压缩包本身是嵌套的(顶层还有个gmp-6.2.1/目录被整体改名了),导致gmp/下面还有一层gmp-6.2.1/。gcc 检查的是gmp/configure这个文件是否存在。
解决:ls gmp/configure确认文件在不在。不在就把目录删掉重新解压,解压后先看一眼顶层结构再改名。前面给的stage_dep函数里加了-f configure判断就是为了提前拦住这种情况。
5.2 make 阶段被 Killed,没有任何错误信息
现象:构建进行到一半,终端只打印一行Killed,或者make: *** [Makefile:xxx] Error 137。
原因:Error 137 = 128 + 9 = SIGKILL,几乎可以确定是内核 OOM killer 干的。gcc 构建的内存峰值远高于日常编译,-j$(nproc)是罪魁。
解决:先降并行度重试,别急着重装系统。JOBS=4 make跑通了再逐步往上加,找到这台机器的实际上限。如果机器内存确实小,加一块 swap 也能救急,只是慢。另外make -j失败后不要直接重跑,先make -k之外的方式确认没有残留的半成品目标文件——保险的做法是rm -rf build从头来。
5.3 编译出的程序拿到目标机上报 GLIBCXX 版本缺失
现象:新 gcc 编出来的二进制,在另一台只有系统自带 gcc 的机器上运行时报version 'GLIBCXX_3.4.26' not found。
原因:动态链接到了新版的libstdc++.so.6,而目标机上的那一个版本更老。gcc 的 C++ 运行时是与编译器版本绑定的,新编译器带的新符号老库不认。这个问题和编译器无关,是部署环节的库版本不一致。
解决:三种路径。一是把$PREFIX/lib64/libstdc++.so.6一起拷到目标机,配合LD_LIBRARY_PATH或编译时加-Wl,-rpath,/opt/gcc-x.y.z/lib64;二是静态链接-static-libstdc++ -static-libgcc,代价是体积变大;三是在构建机上用-D_GLIBCXX_USE_CXX11_ABI=0兼容老 ABI,但这只解决 string 双 ABI 问题,不解决新符号缺失。选哪种取决于目标机能不能改环境。
5.4 make install 之后 gcc -v 还是老版本
现象:安装日志一切正常,$PREFIX/bin/gcc也存在,但终端里敲gcc -v显示的仍是系统自带版本。
原因:PATH 里/usr/bin排在前面,或者 bash 缓存了旧的可执行文件路径(hash表)。后者更隐蔽,因为which gcc可能已经指向新路径,实际执行还是旧的。
解决:先hash -r清缓存,再看echo $PATH。正确的做法是在/etc/profile.d/下放一个脚本,把$PREFIX/bin插到 PATH 最前面,而不是追加到末尾。同时确认$PREFIX/bin/gcc是个真实文件而不是断掉的符号链接。
5.5 装完之后 gcc 找不到自己的头文件
现象:gcc -v显示新版本,但编译一个#include <vector>的简单程序报fatal error: vector: No such file or directory。
原因:安装前缀被移动过,或者被rsync到了另一台机器。gcc 的默认搜索路径在编译时通过--prefix硬编码进二进制,前缀变了路径就全错。另一种原因是只拷贝了bin/目录而没拷include/和lib/gcc/。
解决:整目录搬运,别只挑 bin。如果确实需要改前缀,用-print-search-dirs看实际搜索路径,再用--sysroot或CPLUS_INCLUDE_PATH临时补救,但正解是重新构建。
6. 装完之后的验证与多版本共存:三条命令确认没白折腾
构建跑完不等于装好了,至少要做三项验证才算数。
6.1 三项验证
第一项看编译器自身的配置是否和预期一致:
/opt/gcc-11.3.0/bin/gcc -v 2>&1 | tail -n 3 # 期望看到 --prefix=/opt/gcc-11.3.0、--enable-languages=c,c++ 等第二项验证 C++ 标准支持,直接编一个用到新特性的最小程序,比看版本号可靠:
// probe.cpp:同时验证 C++17 文件系统与结构化绑定 #include <filesystem> #include <iostream> #include <tuple> int main() { auto [a, b] = std::make_tuple(1, 2); // 结构化绑定 std::cout << a + b << " " << std::filesystem::current_path() << "\n"; return 0; }/opt/gcc-11.3.0/bin/g++ -std=c++17 probe.cpp -o probe && ./probe第三项看运行时的 GLIBCXX 符号上限,这决定了你编出来的程序能在多老的机器上跑:
strings /opt/gcc-11.3.0/lib64/libstdc++.so.6 | grep -o 'GLIBCXX_3\.4\.[0-9]*' | sort -V | tail -n 16.2 多版本共存的正确姿势
不要直接改/usr/bin/gcc的指向。我一般用update-alternatives注册新版本,优先级设高一点,需要时切换:
sudo update-alternatives --install /usr/bin/gcc gcc /opt/gcc-11.3.0/bin/gcc 100 \ --slave /usr/bin/g++ g++ /opt/gcc-11.3.0/bin/g++也可以只在构建脚本里用绝对路径调用,完全不碰系统工具链,确定性最高。这两种方式我都用,前者适合交互式开发机,后者适合 CI 和内网构建机。
| 验证项 | 命令 | 通过标准 |
|---|---|---|
| 配置参数 | gcc -v | prefix 与 languages 符合预期 |
| 语言特性 | 编译probe.cpp | 无警告无错误,输出正常 |
| 运行时符号 | strings libstdc++.so.6 | 最高 GLIBCXX 版本满足目标机 |
早期我只在构建机上验证前两项就打包发走,直到有一次目标机跑了一周才因为缺符号崩在半夜——那批程序全都用了std::filesystem,而目标机的 libstdc++ 差了一个小版本。从那以后我每次做完离线安装,都强制把上面三项跑一遍,尤其是第三项,还会顺手把 libstdc++ 的版本号写进交付说明里。多花三分钟,省掉的是半夜被告警叫起来的那种后悔药。希望帮到你。
本文还有配套的精品资源,点击获取