简介:在无法连接互联网的服务器、学校机房或边缘设备中,搭建编译器工具链常因缺少依赖而失败。这份面向红帽系列Linux 6系统的GCC离线安装包共包含十二个文件:十一份软件安装包与一份安装脚本。其中十一份安装包覆盖基础编译器、C++编译器、预处理程序、系统头文件与内核头文件等关键组件,并已处理版本匹配关系;安装脚本则负责自动检查环境、解包并完成安装,使运维人员无需逐条执行命令即可完成部署。软件包整体压缩后约二十四兆字节,体积小巧便于使用移动介质拷入内网。目前已有五百五十六人学习使用,特别适合需要在内网或受限网络环境中搭建开发环境的系统管理员与开发者;同时,通过查看安装包内各文件的依赖结构,也能帮助初学者理解编译器集合的组成和软件包管理的基本思路。1. gcc 离线安装:一台内网裸机的刚需前几天帮某公司机房做环境交付,机器是全新的,最小化安装的 Linux,没有外网权限,gcc --version直接提示 command not found,而所有业务系统都需要编译源码。更麻烦的是,不是一台机器,而是三十台同配置的机器,都得统一装好同样的工具链。我手头没有光盘,也没有内网 yum 镜像,只能靠一个离线包把整个 gcc 编译链端过去。这就是这类“gcc 离线安装包+脚本”资源的典型用途:把 gcc 本体以及所有依赖打包成 RPM 集合,再通过脚本在内网批量安装。适合运维工程师、实施人员、测试环境搭建者,以及所有被内外网隔离折磨过的人。2. 在联网机器上收集 rpm:yumdownloader 与 reposync 的取舍2.1 为什么不能只拷贝 gcc 的单个 rpm很多人第一次做离线安装,会直接在一台有网的机器上yum install -y gcc,然后跑到/var/cache/yum里翻那个 RPM 文件,拷到内网去装。结果是rpm -ivh gcc-xxx.rpm报一堆依赖缺失:libmpc.so.3()(64bit) is needed、libmpfr.so.4()(64bit) is needed、cpp = x.x.x is needed。gcc 从来不是一个孤立软件包,它跟 cpp、binutils、glibc-devel、gmp、mpfr、mpc、isl 这些包绑定得很紧,只拷本体等于只带了发动机没带轮胎。所以第一步不是下载,而是先想清楚需要哪些包。在一个标准的 x86_64 的 CentOS/RHEL 类系统上,gcc 的工具链至少包含这些:gcc、gcc-c++、gcc-gfortran(可选)、cpp、binutils、glibc-devel、libstdc++-devel、libgcc、libgomp,以及被依赖的gmp、mpfr、mpc、isl。这里还不算 kernel-headers,如果机器要编译内核模块,还得补上kernel-devel,但那是另一个话题,不是 gcc 的基本盘。2.2 用 yumdownloader 收集指定包及自动依赖我的常规做法是用yumdownloader,它只下载不安装,也不会去动当前系统的包状态。优点是干净、可控,缺点是必须手动把依赖列全,漏一个在内网就会卡住。下面的命令在联网机器上执行:# 先安装 yum-utils,提供 yumdownloader 命令 yum install -y yum-utils # 建一个目录,后面所有 rpm 都放这里 mkdir -p /tmp/gcc-offline-pkgs # 下载 gcc 及常用工具链,加 --resolve 自动解析依赖 yumdownloader --destdir=/tmp/gcc-offline-pkgs --resolve \ gcc gcc-c++ gcc-gfortran cpp binutils \ glibc-devel libstdc++-devel libgcc libgomp \ gmp-devel mpfr-devel mpc-devel isl-devel这里--resolve是关键参数,它会像yum install一样做依赖解析,把缺的依赖包一起拉到 destdir 里。跟直接yum install不同,它不会安装,只负责下载。--destdir指定输出目录,建议用绝对路径,后面做本地 repo 时路径不容易写错。gcc-gfortran如果业务不需要 Fortran 编译器,完全可以去掉,能少拉一堆依赖。gmp-devel、mpfr-devel这一类带-devel的包,主要是给以后要手工编译其他软件时提供头文件和.so软链,如果纯装 gcc 自身,有些并非强制,但为了一个包一次到位,我一般都会收进去。2.3 用 reposync 拉整个 base 仓库的备选方案如果内网机器数量多、后续还要装别的软件,yumdownloader就不够用了,因为你每装一个软件都得重新收集一次。这种时候建议改用reposync,直接把远程的 base/updates 仓库同步到本地内网服务器,再配合createrepo生成仓库元数据,等于在内网搭建了一个迷你 yum 源。命令如下:# 安装 createrepo 和 yum-utils yum install -y createrepo yum-utils # 只同步 base 仓库到本地目录,带元数据 reposync -p /tmp/yum-mirror --repoid=base --download-metadata # 为镜像目录生成仓库索引 createrepo /tmp/yum-mirror--repoid=base指定同步哪个仓库,可以用yum repolist查看当前系统启用的仓库 ID。--download-metadata会同步 primary、filelists 这类元数据,生成后就能直接做成内网 repo 源。注意reposync会把整个仓库几十 GB 都拉下来,如果只是装个 gcc 没必要,但它确实是一劳永逸的方案。如果磁盘紧张,还可以用--filter或者只同步几个特定包,但那样语法会复杂不少,不适合新手折腾。2.4 收集完成后的自检清单下载完之后别急着打包,先做一遍清点,不然到了内网才发现缺包,来回折腾很浪费时间。我一般会跑这几条检查:# 检查数量、大小和架构 ls -lh /tmp/gcc-offline-pkgs/*.rpm | wc -l du -sh /tmp/gcc-offline-pkgs # 确认都是 x86_64,没有混入 i686 或 noarch rpm -qp --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' /tmp/gcc-offline-pkgs/*.rpm | grep -v 'x86_64\|noarch' || echo "架构全部一致" # 检查签名信息,确保没拉坏包 rpm -K /tmp/gcc-offline-pkgs/*.rpm 2>/dev/null | grep -i 'OK' | wc -l这个检查非常关键。之前我遇到过一台 64 位系统,yumdownloader 因为某个依赖的编译环境问题,意外把 i686 的 glibc-devel 也拉了进来,内网安装时 RPM 对架构非常敏感,i686 和 x86_64 同时出现会导致依赖判断错乱。所以检查架构那一步最好每次都跑。另外用rpm -K校验签名时,如果系统没有导入对应 GPG 公钥会提示 NOKEY,那种情况只要确认 sha256 校验通过即可,不必过分紧张.3. 安装脚本设计:本地 repo 源与幂等安装3.1 rpm -Uvh 直接硬装为什么容易翻车把 rpm 包拷到内网后,最常见的主观想法是:rpm -Uvh /tmp/pkgs/*.rpm,一把梭。但这样做的失败率极高,因为rpm -Uvh不会做依赖解析,它只是逐个安装,遇到依赖缺失就报错退出。更麻烦的是,gcc 依赖链里有循环情况,比如gcc依赖mpc、mpc依赖mpfr、mpfr依赖gmp,表面看是一长串,但实际用rpm -Uvh *.rpm一起装时,RPM 内部会尝试解决顺序问题,多数时候能成,但一旦其中一个包版本未满足另外包的精确要求,整个事务就会回滚,连已装好的也全部撤销。运气坏的时候,还会把 glibc 相关包动到,弄完系统直接起不来,那已经不是翻车是翻沟里了。所以我给配套脚本设计的核心思路是:先建本地 yum 仓库,再让 yum 去解决依赖。yum 是完整的依赖解析器,比人肉排序可靠得多。3.2 用 createrepo 把 rpm 目录变成 yum 源在离线机器上,先把包解压到固定目录,然后用createrepo生成元数据。这里有一个细节:离线机器的软件源里必须提前有createrepo这个工具,否则你连源都建不出来。所以在收集阶段,我就把createrepo和它的依赖python-deltarpm、libxml2-python加进了 yumdownloader 的清单里。# 假设离线包在 /opt/gcc-pkgs 下 cd /opt/gcc-pkgs # 生成 repodata 目录 createrepo . # 写 repo 文件到 /etc/yum.repos.d/ cat > /etc/yum.repos.d/gcc-local.repo << 'EOF' [gcc-local] name=GCC Local Repository baseurl=file:///opt/gcc-pkgs enabled=1 gpgcheck=0 EOF # 清缓存并验证 repo 是否生效 yum clean all yum repolistcreaterepo .会在当前目录生成repodata/文件夹,里面是 primary.xml.gz 等元数据。repo 文件里的gpgcheck=0是因为离线包没有对应的 GPG 公钥,如果设成 1,yum 会拒绝安装并报错。file://协议指向本地目录,注意路径必须和包实际位置一致,写成file:///opt/gcc-pkgs是三个斜杠,少了就加载不到。3.3 脚本:幂等安装、日志记录与退出码写安装脚本时我坚持三个原则:可以重复跑、跑完有日志、失败有明确退出码。很多离线安装脚本都是写死的命令,装过一次再跑就乱套,没有日志排查时全靠猜,这种脚本我不愿意交付给别人。下面这个是精简版的脚本骨架:#!/bin/bash # gcc 离线安装脚本 # 用法: ./install_gcc_offline.sh [pkgs_dir] PKG_DIR="${1:-/opt/gcc-pkgs}" LOG_FILE="/var/log/gcc-offline-install.log" # 判断是否已安装过 gcc if rpm -q gcc >/dev/null 2>&1; then echo "[$(date)] gcc 已安装,跳过" | tee -a "$LOG_FILE" exit 0 fi # 检查 repo 文件是否存在 if [ ! -f /etc/yum.repos.d/gcc-local.repo ]; then echo "[$(date)] 未找到 gcc-local.repo,请先配置本地源" | tee -a "$LOG_FILE" exit 1 fi # 用 yum 安装,自动解析依赖 yum install -y gcc gcc-c++ gcc-gfortran 2>&1 | tee -a "$LOG_FILE" # 验证安装结果 if rpm -q gcc gcc-c++ >/dev/null 2>&1; then echo "[$(date)] gcc 工具链安装成功" | tee -a "$LOG_FILE" exit 0 else echo "[$(date)] 安装失败,检查 /var/log/gcc-offline-install.log" | tee -a "$LOG_FILE" exit 1 fi这个脚本的逻辑很直白:开头用rpm -q gcc判断是否已安装,避免重复装一遍;中间用yum install而不是rpm -Uvh,让 yum 解析本地 repo 里的依赖;最后再验证一次 gcc 是否真的可用。tee -a把命令输出同时打到终端和日志文件,这样出问题可以回看日志。${1:-/opt/gcc-pkgs}是给路径留了一个参数位,默认值是/opt/gcc-pkgs,如果你把包解压到别处,执行时把路径传进去就行。3.4 批量部署时的参数化修改那台服务器是三十台机器,我一般不会一台台跑,而是配合 ansible 的 copy 模块把 rpms 目录和脚本分发下去,再批量执行。脚本里唯一要改的就是PKG_DIR的默认路径,如果内网每台机器的授权目录不同,建议改成从配置文件读路径,而不是硬编码。还一个重要参数是yum install那一行的包名列表,如果不需要 gfortran,删掉即可。这个脚本尽量保持单文件、零依赖,因为它本身就是离线安装用的,如果它再依赖别的东西,就成鸡生蛋问题。4. 依赖链拆解:gcc 从 rpm 到源码编译所需的完整包清单4.1 gcc 的依赖树:底层库的层级关系gcc 的依赖不像普通软件那样只是一条线,而是分三层。最底层是被 gcc 运行库直接调用的数学库,包括gmp(大整数运算)、mpfr(浮点运算)、mpc(复数运算),这三个在 gcc 编译阶段就必须存在,它们的.so文件会被 gcc 自身链接。再上层是isl,这是一个整数线性规划库,GCC 在做循环优化和并行优化时依赖它。最上面才是gcc、cpp、gcc-c++这些编译前端。如果只是用yum install gcc,yum 会把这些底层库自动拉进来,但离线手工收集时很多人会漏掉isl,因为它在老的 CentOS 6 里甚至不是独立包,而是被编进 gcc 的内部目录。到了 CentOS 7/8 上才把它拆成独立isl包。所以收集时不要想当然按文档里常见的依赖列表来,最好用yumdownloader --resolve自动抓,抓完再用rpm -qpR逐个验证一遍:.so是否被满足、依赖包是否都在本地。4.2 编译期依赖与运行期依赖的差异另一个容易错的重点是区分编译期和运行期。glibc-devel提供的是stdio.h、stdlib.h这些头文件,它是编译 C 程序时才需要的,运行时程序用的是/lib64/libc.so.6,这是 glibc 包提供的。所以在离线机器上,如果你只装了 gcc 而没装glibc-devel,gcc -v能正常输出版本号,但编译hello.c时会报stdio.h: No such file or directory。这不是 gcc 坏了,是缺开发头文件。同样的道理适用于libstdc++-devel。gcc-c++包本身含 g++ 编译器和基础运行库,但标准模板库(STL)的头文件在libstdc++-devel里。很多人在内网编译 C++ 项目时报iostream: No such file,就是他只收了gcc-c++忘了libstdc++-devel。至于编译后的程序跑到另一台机器上还需要什么,那是另一套ldd检查的思路,不是离线安装 gcc 重点。4.3 版本匹配:不能用新版 RPM 包覆盖旧系统在联网机器上收集 rpm 时,有个隐含风险:联网机器如果比内网机器系统版本新(比如联网机是 CentOS 8,内网机是 CentOS 7),拉下来的 gcc RPM 拿到内网是装不上的,因为 RPM 打包时依赖的 glibc 版本高于内网系统自带的东西。装的时候会直接报libc.so.6(GLIBC_2.18)(64bit) is needed,然后拒绝安装。收集安装包前,先确认好目标内网系统的版本,最好的做法是找一台和离线机器完全同版本、同小版本的联网机器来采集,或者在内网里随便选一台同样系统的机器,先临时联网,用它自己的 yum 拉包再断网。实在找不到同版本的,也可以退而求其次,用--releasever指定系统版本去收集,比如:# 在联网机器上指定系统版本收集,防止拉到太新的包 yumdownloader --destdir=/tmp/gcc-pkgs --releasever=7 --resolve \ gcc gcc-c++ glibc-devel libstdc++-devel--releasever=7会让 yum 以 CentOS 7 的仓库视图去解析依赖,这样收集到的 RPM 版本匹配 CentOS 7。如果连这个参数都不加,默认用你当前系统的版本,装到低版本系统上就会出现上面的 GLIBC 错误。常见依赖包对应系统版本如下,表格可以在写离线部署文档时直接抄用:目标系统gcc 默认版本glibc 版本注意点CentOS 74.8.52.17gcc 较老,C++11 支持不完整CentOS 88.5.02.28支持 C++17,注意 repo 源差异Ubuntu 18.047.5.02.27包格式为 deb,不适用于 rpm 脚本5. 离线安装避坑:五条翻车经历与排查方法5.1 现象:rpm -ivh 报内存不足,安装到一半被杀原因:同一时间把所有 rpm 用一条命令提交,RPM 会为每个包都做事务检查,内存占用叠加,在低配的 1G 内存机器上直接 OOM。这是我在某项目的 512M 小机器上踩过的坑,三十多个包一起rpm -ivh,进度到一半进程被系统 kill,连已装的包也全部回滚。解决:不要一次性rpm -Uvh *.rpm,改用yum install,yum 是逐个处理事务的,内存峰值低得多。如果一定要用 rpm,把安装批次拆成每次 5-10 个包,且先装底层库再装 gcc 本体。5.2 现象:yum install gcc提示 Nothing to do,但 gcc 命令确实不存在原因:本地 repo 配置文件baseurl指向的路径不对,或者目录里没有repodata子目录。yum 在 repo 加载失败时不会明确报错,而是把该 repo 静默忽略,最后 Nothing to do。解决:用yum repolist看看gcc-local是否出现在启用的仓库列表里。没出现就检查 repo 文件里的路径和file://写法,路径尾部不要加多余的/。另外当心 tar 解压时如果带着顶层目录,包实际在/opt/gcc-pkgs/gcc-pkgs/下,repo 指向/opt/gcc-pkgs就空了。5.3 现象:gcc 能编译 C 程序,但编译 C++ 时报找不到 iostream原因:gcc 本体和 gcc-c++ 是分离的,gcc包只带 C 编译器和基础库。g++命令可能根本不存在,或者存在但缺少 STL 头文件。解决:确认离线包清单里有gcc-c++和libstdc++-devel。验证方法:rpm -q gcc-c++,如果已装但仍然报错,检查g++ -print-search-dirs输出里的 include 路径是否有对应的/usr/include/c++目录。5.4 现象:编译时报cannot find crtbegin.o或crt1.o原因:这是 binutils 和 glibc-devel 的问题,不是 gcc 本体。crt1.o和crtbegin.o分别位于 glibc-devel 和 gcc 包内部,两者缺失就会缺链接启动文件。有人为了省空间故意去掉 glibc-devel,结果就是这个错。解决:把glibc-devel加进包里,并确认 gcc 安装目录下lib/gcc/x86_64-redhat-linux/{version}/crtbegin.o确实存在。有时候还需要binutils的ld命令可用,离线时记得把binutils包也带上。5.5 现象:脚本第二次运行时把 gcc 降级到了旧版本原因:脚本里用了rpm -Uvh直接装旧版 RPM,或者yum install时本地 repo 里的包版本低于已装版本,yum 默认不会降级,但 rpm -Uvh 会强制覆盖为低版本。内网机器如果被升级过,再跑脚本就会出现版本倒退。解决:脚本开头加rpm -q gcc判断,已装则直接跳过。我在脚本骨架里已经写了这个逻辑,执行exit 0前会先检查,避免误伤。6. 装完别急着收工:版本验证与一键卸载回退6.1 三步验证编译环境是否真正可用装完之后光看rpm -qa | grep gcc不算完,编译器这种工具,必须跑一次真实编译才算验证通过。我一般固定走这三个步骤,缺一不可:# 第一步:确认 gcc/g++ 版本在 PATH 里且版本匹配 gcc --version g++ --version # 第二步:写最小 C 程序编译运行 echo '#include <stdio.h>' > /tmp/test.c echo 'int main() { printf("gcc offline ok\n"); return 0; }' >> /tmp/test.c gcc /tmp/test.c -o /tmp/test_c && /tmp/test_c # 第三步:写最小 C++ 程序编译运行,验证 STL 头文件可用 echo '#include <iostream>' > /tmp/test.cpp echo 'int main() { std::cout << "g++ offline ok" << std::endl; return 0; }' >> /tmp/test.cpp g++ /tmp/test.cpp -o /tmp/test_cpp && /tmp/test_cpp注意第二步和第三步必须都跑,很多离线安装 C 的验证能过,死在 C++ 上。gcc --version看到的版本号只说明编译器本身能运行,不代表头文件、链接库都齐全。跑到std::cout这一行才把libstdc++-devel和头文件路径全部验证到了。6.2 留一条回退路径:卸载脚本与包清单快照离线安装最怕的不是装不上,而是装到一半系统环境被搞乱。以前我在一台机器上装新版本 gcc,它把系统原有的libstdc++.so.6覆盖掉,结果所有依赖旧版本 ABI 的程序全部启动报错,那真是灾难。从那以后我每次离线安装前,都会先执行一次包清单快照,把改动前的 rpmdb 状态保存下来,安装完成后再生成一份差异清单,留着做回退依据:# 安装前快照 rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' > /tmp/rpmdb-before.txt # 安装后快照 rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' > /tmp/rpmdb-after.txt # 找出新增和升级的包 diff -u /tmp/rpmdb-before.txt /tmp/rpmdb-after.txt | grep '^+' | cut -c2-把 diff 出来的结果送到卸载脚本里变成rpm -e命令列表。注意卸载顺序要反过来,先卸载 gcc 和 gcc-c++,再考虑动底层库,但底层库尽量不要回滚,因为其他已装的软件可能已经依赖新版本。这套快照方案不能保证百分百还原,但至少给了你一份明确的改动记录,排查和回退都有迹可循。另外一个小技巧:安装完成之后,可以把gcc --version的完整输出、g++ --version的完整输出,连同编译测试的返回码一起写进交付文档里,这样后面接手的人一眼就能看出工具链状态,不用自己再搭一套环境重新验证一遍。从那以后我每次部署完 gcc 都强制走一遍这个三步验证流程,并把版本号记成文件放到/etc/gcc-install-info.txt里,这样后面再出问题,至少不用猜测机器上装的是哪个版本,希望这套方法和避坑记录能帮到你。本文还有配套的精品资源,点击获取
简介:在无法连接互联网的服务器、学校机房或边缘设备中,搭建编译器工具链常因缺少依赖而失败。这份面向红帽系列Linux 6系统的GCC离线安装包共包含十二个文件:十一份软件安装包与一份安装脚本。其中十一份安装包覆盖基础编译器、C++编译器、预处理程序、系统头文件与内核头文件等关键组件,并已处理版本匹配关系;安装脚本则负责自动检查环境、解包并完成安装,使运维人员无需逐条执行命令即可完成部署。软件包整体压缩后约二十四兆字节,体积小巧便于使用移动介质拷入内网。目前已有五百五十六人学习使用,特别适合需要在内网或受限网络环境中搭建开发环境的系统管理员与开发者;同时,通过查看安装包内各文件的依赖结构,也能帮助初学者理解编译器集合的组成和软件包管理的基本思路。1. gcc 离线安装:一台内网裸机的刚需前几天帮某公司机房做环境交付,机器是全新的,最小化安装的 Linux,没有外网权限,gcc --version直接提示 command not found,而所有业务系统都需要编译源码。更麻烦的是,不是一台机器,而是三十台同配置的机器,都得统一装好同样的工具链。我手头没有光盘,也没有内网 yum 镜像,只能靠一个离线包把整个 gcc 编译链端过去。这就是这类“gcc 离线安装包+脚本”资源的典型用途:把 gcc 本体以及所有依赖打包成 RPM 集合,再通过脚本在内网批量安装。适合运维工程师、实施人员、测试环境搭建者,以及所有被内外网隔离折磨过的人。2. 在联网机器上收集 rpm:yumdownloader 与 reposync 的取舍2.1 为什么不能只拷贝 gcc 的单个 rpm很多人第一次做离线安装,会直接在一台有网的机器上yum install -y gcc,然后跑到/var/cache/yum里翻那个 RPM 文件,拷到内网去装。结果是rpm -ivh gcc-xxx.rpm报一堆依赖缺失:libmpc.so.3()(64bit) is needed、libmpfr.so.4()(64bit) is needed、cpp = x.x.x is needed。gcc 从来不是一个孤立软件包,它跟 cpp、binutils、glibc-devel、gmp、mpfr、mpc、isl 这些包绑定得很紧,只拷本体等于只带了发动机没带轮胎。所以第一步不是下载,而是先想清楚需要哪些包。在一个标准的 x86_64 的 CentOS/RHEL 类系统上,gcc 的工具链至少包含这些:gcc、gcc-c++、gcc-gfortran(可选)、cpp、binutils、glibc-devel、libstdc++-devel、libgcc、libgomp,以及被依赖的gmp、mpfr、mpc、isl。这里还不算 kernel-headers,如果机器要编译内核模块,还得补上kernel-devel,但那是另一个话题,不是 gcc 的基本盘。2.2 用 yumdownloader 收集指定包及自动依赖我的常规做法是用yumdownloader,它只下载不安装,也不会去动当前系统的包状态。优点是干净、可控,缺点是必须手动把依赖列全,漏一个在内网就会卡住。下面的命令在联网机器上执行:# 先安装 yum-utils,提供 yumdownloader 命令 yum install -y yum-utils # 建一个目录,后面所有 rpm 都放这里 mkdir -p /tmp/gcc-offline-pkgs # 下载 gcc 及常用工具链,加 --resolve 自动解析依赖 yumdownloader --destdir=/tmp/gcc-offline-pkgs --resolve \ gcc gcc-c++ gcc-gfortran cpp binutils \ glibc-devel libstdc++-devel libgcc libgomp \ gmp-devel mpfr-devel mpc-devel isl-devel这里--resolve是关键参数,它会像yum install一样做依赖解析,把缺的依赖包一起拉到 destdir 里。跟直接yum install不同,它不会安装,只负责下载。--destdir指定输出目录,建议用绝对路径,后面做本地 repo 时路径不容易写错。gcc-gfortran如果业务不需要 Fortran 编译器,完全可以去掉,能少拉一堆依赖。gmp-devel、mpfr-devel这一类带-devel的包,主要是给以后要手工编译其他软件时提供头文件和.so软链,如果纯装 gcc 自身,有些并非强制,但为了一个包一次到位,我一般都会收进去。2.3 用 reposync 拉整个 base 仓库的备选方案如果内网机器数量多、后续还要装别的软件,yumdownloader就不够用了,因为你每装一个软件都得重新收集一次。这种时候建议改用reposync,直接把远程的 base/updates 仓库同步到本地内网服务器,再配合createrepo生成仓库元数据,等于在内网搭建了一个迷你 yum 源。命令如下:# 安装 createrepo 和 yum-utils yum install -y createrepo yum-utils # 只同步 base 仓库到本地目录,带元数据 reposync -p /tmp/yum-mirror --repoid=base --download-metadata # 为镜像目录生成仓库索引 createrepo /tmp/yum-mirror--repoid=base指定同步哪个仓库,可以用yum repolist查看当前系统启用的仓库 ID。--download-metadata会同步 primary、filelists 这类元数据,生成后就能直接做成内网 repo 源。注意reposync会把整个仓库几十 GB 都拉下来,如果只是装个 gcc 没必要,但它确实是一劳永逸的方案。如果磁盘紧张,还可以用--filter或者只同步几个特定包,但那样语法会复杂不少,不适合新手折腾。2.4 收集完成后的自检清单下载完之后别急着打包,先做一遍清点,不然到了内网才发现缺包,来回折腾很浪费时间。我一般会跑这几条检查:# 检查数量、大小和架构 ls -lh /tmp/gcc-offline-pkgs/*.rpm | wc -l du -sh /tmp/gcc-offline-pkgs # 确认都是 x86_64,没有混入 i686 或 noarch rpm -qp --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' /tmp/gcc-offline-pkgs/*.rpm | grep -v 'x86_64\|noarch' || echo "架构全部一致" # 检查签名信息,确保没拉坏包 rpm -K /tmp/gcc-offline-pkgs/*.rpm 2>/dev/null | grep -i 'OK' | wc -l这个检查非常关键。之前我遇到过一台 64 位系统,yumdownloader 因为某个依赖的编译环境问题,意外把 i686 的 glibc-devel 也拉了进来,内网安装时 RPM 对架构非常敏感,i686 和 x86_64 同时出现会导致依赖判断错乱。所以检查架构那一步最好每次都跑。另外用rpm -K校验签名时,如果系统没有导入对应 GPG 公钥会提示 NOKEY,那种情况只要确认 sha256 校验通过即可,不必过分紧张.3. 安装脚本设计:本地 repo 源与幂等安装3.1 rpm -Uvh 直接硬装为什么容易翻车把 rpm 包拷到内网后,最常见的主观想法是:rpm -Uvh /tmp/pkgs/*.rpm,一把梭。但这样做的失败率极高,因为rpm -Uvh不会做依赖解析,它只是逐个安装,遇到依赖缺失就报错退出。更麻烦的是,gcc 依赖链里有循环情况,比如gcc依赖mpc、mpc依赖mpfr、mpfr依赖gmp,表面看是一长串,但实际用rpm -Uvh *.rpm一起装时,RPM 内部会尝试解决顺序问题,多数时候能成,但一旦其中一个包版本未满足另外包的精确要求,整个事务就会回滚,连已装好的也全部撤销。运气坏的时候,还会把 glibc 相关包动到,弄完系统直接起不来,那已经不是翻车是翻沟里了。所以我给配套脚本设计的核心思路是:先建本地 yum 仓库,再让 yum 去解决依赖。yum 是完整的依赖解析器,比人肉排序可靠得多。3.2 用 createrepo 把 rpm 目录变成 yum 源在离线机器上,先把包解压到固定目录,然后用createrepo生成元数据。这里有一个细节:离线机器的软件源里必须提前有createrepo这个工具,否则你连源都建不出来。所以在收集阶段,我就把createrepo和它的依赖python-deltarpm、libxml2-python加进了 yumdownloader 的清单里。# 假设离线包在 /opt/gcc-pkgs 下 cd /opt/gcc-pkgs # 生成 repodata 目录 createrepo . # 写 repo 文件到 /etc/yum.repos.d/ cat > /etc/yum.repos.d/gcc-local.repo << 'EOF' [gcc-local] name=GCC Local Repository baseurl=file:///opt/gcc-pkgs enabled=1 gpgcheck=0 EOF # 清缓存并验证 repo 是否生效 yum clean all yum repolistcreaterepo .会在当前目录生成repodata/文件夹,里面是 primary.xml.gz 等元数据。repo 文件里的gpgcheck=0是因为离线包没有对应的 GPG 公钥,如果设成 1,yum 会拒绝安装并报错。file://协议指向本地目录,注意路径必须和包实际位置一致,写成file:///opt/gcc-pkgs是三个斜杠,少了就加载不到。3.3 脚本:幂等安装、日志记录与退出码写安装脚本时我坚持三个原则:可以重复跑、跑完有日志、失败有明确退出码。很多离线安装脚本都是写死的命令,装过一次再跑就乱套,没有日志排查时全靠猜,这种脚本我不愿意交付给别人。下面这个是精简版的脚本骨架:#!/bin/bash # gcc 离线安装脚本 # 用法: ./install_gcc_offline.sh [pkgs_dir] PKG_DIR="${1:-/opt/gcc-pkgs}" LOG_FILE="/var/log/gcc-offline-install.log" # 判断是否已安装过 gcc if rpm -q gcc >/dev/null 2>&1; then echo "[$(date)] gcc 已安装,跳过" | tee -a "$LOG_FILE" exit 0 fi # 检查 repo 文件是否存在 if [ ! -f /etc/yum.repos.d/gcc-local.repo ]; then echo "[$(date)] 未找到 gcc-local.repo,请先配置本地源" | tee -a "$LOG_FILE" exit 1 fi # 用 yum 安装,自动解析依赖 yum install -y gcc gcc-c++ gcc-gfortran 2>&1 | tee -a "$LOG_FILE" # 验证安装结果 if rpm -q gcc gcc-c++ >/dev/null 2>&1; then echo "[$(date)] gcc 工具链安装成功" | tee -a "$LOG_FILE" exit 0 else echo "[$(date)] 安装失败,检查 /var/log/gcc-offline-install.log" | tee -a "$LOG_FILE" exit 1 fi这个脚本的逻辑很直白:开头用rpm -q gcc判断是否已安装,避免重复装一遍;中间用yum install而不是rpm -Uvh,让 yum 解析本地 repo 里的依赖;最后再验证一次 gcc 是否真的可用。tee -a把命令输出同时打到终端和日志文件,这样出问题可以回看日志。${1:-/opt/gcc-pkgs}是给路径留了一个参数位,默认值是/opt/gcc-pkgs,如果你把包解压到别处,执行时把路径传进去就行。3.4 批量部署时的参数化修改那台服务器是三十台机器,我一般不会一台台跑,而是配合 ansible 的 copy 模块把 rpms 目录和脚本分发下去,再批量执行。脚本里唯一要改的就是PKG_DIR的默认路径,如果内网每台机器的授权目录不同,建议改成从配置文件读路径,而不是硬编码。还一个重要参数是yum install那一行的包名列表,如果不需要 gfortran,删掉即可。这个脚本尽量保持单文件、零依赖,因为它本身就是离线安装用的,如果它再依赖别的东西,就成鸡生蛋问题。4. 依赖链拆解:gcc 从 rpm 到源码编译所需的完整包清单4.1 gcc 的依赖树:底层库的层级关系gcc 的依赖不像普通软件那样只是一条线,而是分三层。最底层是被 gcc 运行库直接调用的数学库,包括gmp(大整数运算)、mpfr(浮点运算)、mpc(复数运算),这三个在 gcc 编译阶段就必须存在,它们的.so文件会被 gcc 自身链接。再上层是isl,这是一个整数线性规划库,GCC 在做循环优化和并行优化时依赖它。最上面才是gcc、cpp、gcc-c++这些编译前端。如果只是用yum install gcc,yum 会把这些底层库自动拉进来,但离线手工收集时很多人会漏掉isl,因为它在老的 CentOS 6 里甚至不是独立包,而是被编进 gcc 的内部目录。到了 CentOS 7/8 上才把它拆成独立isl包。所以收集时不要想当然按文档里常见的依赖列表来,最好用yumdownloader --resolve自动抓,抓完再用rpm -qpR逐个验证一遍:.so是否被满足、依赖包是否都在本地。4.2 编译期依赖与运行期依赖的差异另一个容易错的重点是区分编译期和运行期。glibc-devel提供的是stdio.h、stdlib.h这些头文件,它是编译 C 程序时才需要的,运行时程序用的是/lib64/libc.so.6,这是 glibc 包提供的。所以在离线机器上,如果你只装了 gcc 而没装glibc-devel,gcc -v能正常输出版本号,但编译hello.c时会报stdio.h: No such file or directory。这不是 gcc 坏了,是缺开发头文件。同样的道理适用于libstdc++-devel。gcc-c++包本身含 g++ 编译器和基础运行库,但标准模板库(STL)的头文件在libstdc++-devel里。很多人在内网编译 C++ 项目时报iostream: No such file,就是他只收了gcc-c++忘了libstdc++-devel。至于编译后的程序跑到另一台机器上还需要什么,那是另一套ldd检查的思路,不是离线安装 gcc 重点。4.3 版本匹配:不能用新版 RPM 包覆盖旧系统在联网机器上收集 rpm 时,有个隐含风险:联网机器如果比内网机器系统版本新(比如联网机是 CentOS 8,内网机是 CentOS 7),拉下来的 gcc RPM 拿到内网是装不上的,因为 RPM 打包时依赖的 glibc 版本高于内网系统自带的东西。装的时候会直接报libc.so.6(GLIBC_2.18)(64bit) is needed,然后拒绝安装。收集安装包前,先确认好目标内网系统的版本,最好的做法是找一台和离线机器完全同版本、同小版本的联网机器来采集,或者在内网里随便选一台同样系统的机器,先临时联网,用它自己的 yum 拉包再断网。实在找不到同版本的,也可以退而求其次,用--releasever指定系统版本去收集,比如:# 在联网机器上指定系统版本收集,防止拉到太新的包 yumdownloader --destdir=/tmp/gcc-pkgs --releasever=7 --resolve \ gcc gcc-c++ glibc-devel libstdc++-devel--releasever=7会让 yum 以 CentOS 7 的仓库视图去解析依赖,这样收集到的 RPM 版本匹配 CentOS 7。如果连这个参数都不加,默认用你当前系统的版本,装到低版本系统上就会出现上面的 GLIBC 错误。常见依赖包对应系统版本如下,表格可以在写离线部署文档时直接抄用:目标系统gcc 默认版本glibc 版本注意点CentOS 74.8.52.17gcc 较老,C++11 支持不完整CentOS 88.5.02.28支持 C++17,注意 repo 源差异Ubuntu 18.047.5.02.27包格式为 deb,不适用于 rpm 脚本5. 离线安装避坑:五条翻车经历与排查方法5.1 现象:rpm -ivh 报内存不足,安装到一半被杀原因:同一时间把所有 rpm 用一条命令提交,RPM 会为每个包都做事务检查,内存占用叠加,在低配的 1G 内存机器上直接 OOM。这是我在某项目的 512M 小机器上踩过的坑,三十多个包一起rpm -ivh,进度到一半进程被系统 kill,连已装的包也全部回滚。解决:不要一次性rpm -Uvh *.rpm,改用yum install,yum 是逐个处理事务的,内存峰值低得多。如果一定要用 rpm,把安装批次拆成每次 5-10 个包,且先装底层库再装 gcc 本体。5.2 现象:yum install gcc提示 Nothing to do,但 gcc 命令确实不存在原因:本地 repo 配置文件baseurl指向的路径不对,或者目录里没有repodata子目录。yum 在 repo 加载失败时不会明确报错,而是把该 repo 静默忽略,最后 Nothing to do。解决:用yum repolist看看gcc-local是否出现在启用的仓库列表里。没出现就检查 repo 文件里的路径和file://写法,路径尾部不要加多余的/。另外当心 tar 解压时如果带着顶层目录,包实际在/opt/gcc-pkgs/gcc-pkgs/下,repo 指向/opt/gcc-pkgs就空了。5.3 现象:gcc 能编译 C 程序,但编译 C++ 时报找不到 iostream原因:gcc 本体和 gcc-c++ 是分离的,gcc包只带 C 编译器和基础库。g++命令可能根本不存在,或者存在但缺少 STL 头文件。解决:确认离线包清单里有gcc-c++和libstdc++-devel。验证方法:rpm -q gcc-c++,如果已装但仍然报错,检查g++ -print-search-dirs输出里的 include 路径是否有对应的/usr/include/c++目录。5.4 现象:编译时报cannot find crtbegin.o或crt1.o原因:这是 binutils 和 glibc-devel 的问题,不是 gcc 本体。crt1.o和crtbegin.o分别位于 glibc-devel 和 gcc 包内部,两者缺失就会缺链接启动文件。有人为了省空间故意去掉 glibc-devel,结果就是这个错。解决:把glibc-devel加进包里,并确认 gcc 安装目录下lib/gcc/x86_64-redhat-linux/{version}/crtbegin.o确实存在。有时候还需要binutils的ld命令可用,离线时记得把binutils包也带上。5.5 现象:脚本第二次运行时把 gcc 降级到了旧版本原因:脚本里用了rpm -Uvh直接装旧版 RPM,或者yum install时本地 repo 里的包版本低于已装版本,yum 默认不会降级,但 rpm -Uvh 会强制覆盖为低版本。内网机器如果被升级过,再跑脚本就会出现版本倒退。解决:脚本开头加rpm -q gcc判断,已装则直接跳过。我在脚本骨架里已经写了这个逻辑,执行exit 0前会先检查,避免误伤。6. 装完别急着收工:版本验证与一键卸载回退6.1 三步验证编译环境是否真正可用装完之后光看rpm -qa | grep gcc不算完,编译器这种工具,必须跑一次真实编译才算验证通过。我一般固定走这三个步骤,缺一不可:# 第一步:确认 gcc/g++ 版本在 PATH 里且版本匹配 gcc --version g++ --version # 第二步:写最小 C 程序编译运行 echo '#include <stdio.h>' > /tmp/test.c echo 'int main() { printf("gcc offline ok\n"); return 0; }' >> /tmp/test.c gcc /tmp/test.c -o /tmp/test_c && /tmp/test_c # 第三步:写最小 C++ 程序编译运行,验证 STL 头文件可用 echo '#include <iostream>' > /tmp/test.cpp echo 'int main() { std::cout << "g++ offline ok" << std::endl; return 0; }' >> /tmp/test.cpp g++ /tmp/test.cpp -o /tmp/test_cpp && /tmp/test_cpp注意第二步和第三步必须都跑,很多离线安装 C 的验证能过,死在 C++ 上。gcc --version看到的版本号只说明编译器本身能运行,不代表头文件、链接库都齐全。跑到std::cout这一行才把libstdc++-devel和头文件路径全部验证到了。6.2 留一条回退路径:卸载脚本与包清单快照离线安装最怕的不是装不上,而是装到一半系统环境被搞乱。以前我在一台机器上装新版本 gcc,它把系统原有的libstdc++.so.6覆盖掉,结果所有依赖旧版本 ABI 的程序全部启动报错,那真是灾难。从那以后我每次离线安装前,都会先执行一次包清单快照,把改动前的 rpmdb 状态保存下来,安装完成后再生成一份差异清单,留着做回退依据:# 安装前快照 rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' > /tmp/rpmdb-before.txt # 安装后快照 rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' > /tmp/rpmdb-after.txt # 找出新增和升级的包 diff -u /tmp/rpmdb-before.txt /tmp/rpmdb-after.txt | grep '^+' | cut -c2-把 diff 出来的结果送到卸载脚本里变成rpm -e命令列表。注意卸载顺序要反过来,先卸载 gcc 和 gcc-c++,再考虑动底层库,但底层库尽量不要回滚,因为其他已装的软件可能已经依赖新版本。这套快照方案不能保证百分百还原,但至少给了你一份明确的改动记录,排查和回退都有迹可循。另外一个小技巧:安装完成之后,可以把gcc --version的完整输出、g++ --version的完整输出,连同编译测试的返回码一起写进交付文档里,这样后面接手的人一眼就能看出工具链状态,不用自己再搭一套环境重新验证一遍。从那以后我每次部署完 gcc 都强制走一遍这个三步验证流程,并把版本号记成文件放到/etc/gcc-install-info.txt里,这样后面再出问题,至少不用猜测机器上装的是哪个版本,希望这套方法和避坑记录能帮到你。本文还有配套的精品资源,点击获取