ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GCC离线安装全包脚本:依赖闭包、本地仓库与校验

GCC离线安装全包脚本:依赖闭包、本地仓库与校验

简介:面向无外网环境的内网服务器与远程主机,这份压缩包提供了一套完整的 GCC 离线安装方案,主要服务于需要在 CentOS/RHEL 6(x86_64)上搭建编译环境的运维人员与开发者,用于解决无法通过 yum 在线拉取编译工具链、依赖包难以凑齐的问题。包内共 12 个文件,由 11 个 rpm 安装包与 1 个 sh 安装脚本组成,整体约 24.09MB;rpm 覆盖 gcc、gcc-c++ 编译器主体与 cpp 预处理器,glibc-devel、kernel-headers 等开发头文件,以及 mpfr、ppl、cloog-ppl 等数学与循环优化依赖库,基本构成一套自洽的依赖闭包,脚本则按序完成解压、依赖处理与安装,省去逐个查找组件的繁琐。已有 558 人学习下载,适合在隔离网络中复现编译环境、排查缺失依赖时参照,也为远程教育环境与离线部署场景提供可直接复用的工具组合与依赖清单。

1. gcc 离线安装所有包以及脚本:先认清依赖闭包比拷 gcc 本体更要命

内网机器上做 gcc 离线安装所有包以及脚本,最容易翻车的点往往不是 gcc 本体没拷过去,而是依赖闭包缺了一个看起来不相干的库。表面报错是cannot find -lmpfr,根因却是收集包时只盯着 gcc、gcc-c++,漏了 mpfr、mpc、isl 这类运行期和开发期依赖。

这个资源解决的是“把该收的包收全、用脚本装得可重复、出问题能查”三件事。它不只是一份包列表,还包括收集、安装、校验三段脚本,目标是把离线工具链交付变成可复现流程,而不是到现场凭记忆补包。

适合需要在无外网环境交付编译工具链的从业者,也适合第一次接手离线编译环境的新手照着抄。别急着拷包,先把依赖链和脚本边界讲清。

2. 收全离线依赖:从包管理器缓存到闭包清单的落地做法

离线包不是把 gcc 一个包单独下载就完事,真正要收的是依赖闭包。闭包里既有直接依赖,也有间接依赖,还要区分运行期库和开发期头文件包。只收gcc不收gcc-c++,C 程序能编,C++ 程序立刻报cannot find -lstdc++;只收运行库不收*-devel,编译时又找不到头文件。先把路线选对,再谈下载命令。

2.1 先判断目标机走二进制包还是源码编译

二进制包路线适合目标机发行版和架构与联网收集机一致、允许使用系统包管理器、对 gcc 版本没有特殊要求的场景。它的优点是快,依赖关系由包管理器算好,装完就能用;缺点是受目标机软件源版本限制,收集机系统版本高于目标机时容易碰到 GLIBC 符号缺失。

源码编译路线适合目标机版本较旧、需要特定编译选项、或者不允许替换系统工具链的场景。它可控,但依赖链长,通常要按 gmp、mpfr、mpc、isl、zlib、binutils、gcc 的顺序构建。源码路线的前缀要独立,常见做法是装到/opt/offline-gcc或用户目录下,不覆盖系统/usr。

路线适用场景需要重点收的包/源码主要风险
二进制包目标机与收集机同发行版、同架构gcc、g++、make、binutils、开发库架构不匹配、GLIBC 版本过高、仓库索引缺失
源码编译需要指定版本或独立前缀gmp、mpfr、mpc、isl、zlib、binutils、gcc依赖顺序错、动态库路径未设置、构建时间长

我一般会先问三个问题:目标机uname -m是什么,目标机系统大版本和收集机是否一致,现场有没有 root。三个答案里有一个不确定,就优先考虑源码编译或找同版本目标机收集包。离线环境没有外网,后悔药很少,收集阶段宁可多收一个包,也不要在现场猜。

2.2 用包管理器把依赖闭包下载到本地目录

RPM 系和 DEB 系的下载方式不一样,脚本里要分开处理。下面这段collect.sh片段把两种分支放在一起,输出目录由第一个参数传入,包数组按目标机实际需求增减。

#!/usr/bin/env bash set -euo pipefail DEST="${1:?用法: collect.sh <输出目录>}" mkdir -p "$DEST" if command -v dnf >/dev/null 2>&1; then PKGS=(gcc gcc-c++ make binutils glibc-devel libstdc++-devel) dnf download --resolve --alldeps --destdir "$DEST" "${PKGS[@]}" elif command -v yumdownloader >/dev/null 2>&1; then PKGS=(gcc gcc-c++ make binutils glibc-devel libstdc++-devel) yumdownloader --resolve --destdir "$DEST" "${PKGS[@]}" elif command -v apt-get >/dev/null 2>&1; then PKGS=(gcc g++ make binutils libc6-dev libstdc++-dev) apt-get -o Dir::Cache::archives="$DEST" --download-only -y install "${PKGS[@]}" else echo "未识别的包管理器" >&2 exit 2 fi find "$DEST" -maxdepth 1 -type f -printf '%f\n' | sort > "$DEST/package-list.txt"

--resolve让包管理器解析依赖,--alldeps把依赖一起拉下来,--destdir指定下载目录。DEB 分支用Dir::Cache::archives把 deb 缓存重定向到目标目录,--download-only表示只下载不安装。package-list.txt记录文件名,方便到目标机核对。

参数怎么改:如果现场还要gdb、cmake、autoconf,直接往PKGS数组里加;如果目标机是不同架构,收集机必须加对应架构参数或直接在目标架构机器上收集。--alldeps可能把一些已安装的包也拉下来,但离线包不怕多,怕少。真正要避免的是收集机软件源版本高于目标机,这种包拷过去可能直接报 GLIBC 符号缺失。

提示:收集前先确认目标机的包管理器版本和软件源大版本。用高版本系统收集低版本系统的包,是离线安装里最常见的血泪经验。

2.3 生成清单与校验文件

包收完后要生成manifest.sha256,否则到现场少一个包、坏一个包都说不清。清单里用相对路径,校验时先cd到包目录,避免绝对路径在另一台机器上对不上。

#!/usr/bin/env bash set -euo pipefail DEST="${1:?用法: manifest.sh <包目录>}" cd "$DEST" find . -maxdepth 1 -type f \( -name '*.rpm' -o -name '*.deb' \) -print0 | sort -z | xargs -0 sha256sum > manifest.sha256 sha256sum -c manifest.sha256

find只取当前目录的 rpm 和 deb,-print0配合xargs -0处理文件名里的空格,sort -z保证清单顺序稳定。sha256sum -c是自校验,生成完立刻跑一遍,确认清单本身没写错。到目标机后先执行sha256sum -c manifest.sha256,再动安装脚本。

清单之外还可以生成一份packages.tsv,记录包名、架构、文件名,方便现场用grep查。不要只依赖包文件名,RPM 文件名里可能带发行版标记,DEB 文件名里可能带架构后缀,人工看容易混。收集脚本把包收全,清单脚本把完整性锁住,这两步做完再进安装脚本。

3. 安装脚本拆解:幂等、本地仓库与源码编译两条路

安装脚本不是把命令堆在一起,离线环境里它要承担排错入口的作用。现场没有外网,脚本必须告诉操作者卡在哪一步:是包不匹配、锁冲突、还是编译失败。脚本要带参数、日志、锁和错误码,重复执行不能把已有环境弄坏。下面按二进制包和源码编译两条路拆。

3.1 脚本骨架:参数、日志、锁与错误码

先给install.sh一个统一骨架。日志写到logs/下,锁文件防止两个安装进程同时跑,trap在失败时打印行号,MODE参数决定走rpm、deb还是source分支。

#!/usr/bin/env bash set -Eeuo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" LOG="${ROOT}/logs/install-$(date +%Y%m%d-%H%M%S).log" LOCK="/tmp/offline-gcc.lock" mkdir -p "${ROOT}/logs" exec > >(tee -a "$LOG") 2>&1 trap 'echo "失败行号: $LINENO,退出码: $?" >&2' ERR exec 9>"$LOCK" if ! flock -n 9; then echo "另一个安装进程正在运行" >&2 exit 3 fi MODE="${1:-auto}" PREFIX="${PREFIX:-/opt/offline-gcc}"

set -Eeuo pipefail让脚本在未定义变量、管道失败、命令失败时退出;trap ... ERR打印失败行号,离线现场靠这个定位比看一屏命令输出快;flock -n 9拿不到锁就退出,避免两个终端同时装包把包管理器事务搅乱。MODE默认auto,脚本里再根据包目录和系统命令判断实际分支。

错误码约定清楚:2 表示参数错误,3 表示锁冲突,4 表示包不匹配或仓库生成失败,5 表示源码编译失败。现场看到退出码就能先判断方向,不用从头翻日志。PREFIX只在源码编译分支使用,无 root 时改成$HOME/toolchain,不要在系统目录里做实验。

3.2 二进制包安装分支:本地仓库做法

RPM 系不要直接rpm -ivh *.rpm,包之间有依赖顺序,人工排序容易漏。常见做法是用createrepo_c生成本地仓库,再让包管理器从file://源安装。DEB 系用dpkg-scanpackages生成Packages索引,再写一个只指向本地目录的 sources 文件。

install_rpm_repo() { local repo_dir="${ROOT}/packages/rpm" local repo_file="/etc/yum.repos.d/offline-gcc.repo" if command -v createrepo_c >/dev/null 2>&1; then createrepo_c "$repo_dir" elif command -v createrepo >/dev/null 2>&1; then createrepo "$repo_dir" else echo "缺少 createrepo_c,无法生成本地仓库" >&2 return 4 fi cat > "$repo_file" <<EOF [offline-gcc] name=offline-gcc baseurl=file://${repo_dir} enabled=1 gpgcheck=0 EOF dnf --disablerepo='*' --enablerepo='offline-gcc' -y install gcc gcc-c++ make binutils }

createrepo_c在包目录下生成repodata,baseurl=file://指向本地路径,--disablerepo='*'禁用其他源,--enablerepo='offline-gcc'只启用本地源,防止安装过程中偷偷联网。gpgcheck=0是因为离线包通常没有导入签名,生产环境更稳妥的做法是导入公钥并开启校验,而不是长期关掉。

DEB 分支如下:

install_deb_repo() { local repo_dir="${ROOT}/packages/deb" local list_file="/etc/apt/sources.list.d/offline-gcc.list" (cd "$repo_dir" && dpkg-scanpackages . /dev/null > Packages) gzip -kf "${repo_dir}/Packages" echo "deb [trusted=yes] file:${repo_dir} ./" > "$list_file" apt-get -o Dir::Etc::sourcelist="$list_file" \ -o Dir::Etc::sourceparts="-" \ -o APT::Get::List-Cleanup="0" update apt-get -y install gcc g++ make binutils }

dpkg-scanpackages生成索引,gzip -kf生成Packages.gz;[trusted=yes]跳过签名校验,离线环境常见,但要在交付说明里写清楚;Dir::Etc::sourcelist只读这一个本地源,sourceparts="-"禁用其他片段,避免apt-get update仍然去访问外部源。List-Cleanup="0"防止更新时清掉本地列表。

这两个函数都要放在MODE判断里,不要无条件执行。脚本先检测packages/rpm或packages/deb是否存在,再决定调用哪个分支。现场如果两个目录都有,按目标机包管理器类型选,不要混装。

3.3 源码编译分支:依赖顺序与前缀

源码编译最容易错在顺序。gmp 先编,mpfr 依赖 gmp,mpc 依赖 gmp 和 mpfr,isl 依赖 gmp,binutils 提供汇编器和链接器,gcc 最后编。前缀统一到$PREFIX,gcc 配置时用--with-gmp、--with-mpfr、--with-mpc、--with-isl指向同一个前缀,避免链接到系统旧库。

build_dep() { local name="$1" local src="${ROOT}/sources/${name}" local build="${src}/build" rm -rf "$build" mkdir -p "$build" cd "$build" "${src}/configure" --prefix="$PREFIX" --disable-shared --enable-static make -j"$(nproc)" make install } build_dep gmp build_dep mpfr build_dep mpc build_dep isl

--disable-shared --enable-static让依赖库以静态形式安装,减少运行时动态库路径问题;如果现场需要共享库,把--disable-shared去掉,但后面必须设置LD_LIBRARY_PATH。-j"$(nproc)"用满 CPU 核数,内存小的机器要改成-j2或-j1,否则可能在编译中途被 OOM 杀掉。

binutils 和 gcc 单独编:

cd "${ROOT}/sources/binutils/build" ../configure --prefix="$PREFIX" --disable-nls --disable-werror make -j"$(nproc)" make install cd "${ROOT}/sources/gcc/build" ../configure \ --prefix="$PREFIX" \ --with-gmp="$PREFIX" \ --with-mpfr="$PREFIX" \ --with-mpc="$PREFIX" \ --with-isl="$PREFIX" \ --disable-multilib \ --enable-languages=c,c++ \ --disable-bootstrap make -j"$(nproc)" make install

--disable-nls关掉本地化,减少依赖;--disable-werror防止新编译器把警告当错误;--disable-multilib只构建目标架构,除非现场明确需要 32 位和 64 位两套库;--enable-languages=c,c++按需加fortran等语言;--disable-bootstrap加快构建,正式交付验证时可以去掉,用自举方式确认编译器稳定。

编完后把$PREFIX/bin加进PATH,把$PREFIX/lib和$PREFIX/lib64加进LD_LIBRARY_PATH与LIBRARY_PATH。不要直接覆盖/usr/bin/gcc,独立前缀是给自己留后路。

3.4 幂等与回滚

幂等不是重复安装,而是重复执行不破坏已有环境。源码编译分支先检查$PREFIX/bin/gcc是否存在,存在就跳过;包管理器分支依赖事务,失败后不要直接删锁文件,先确认没有残留进程。

STATE_DIR="${ROOT}/.state" mkdir -p "$STATE_DIR" if [[ -x "${PREFIX}/bin/gcc" ]]; then echo "目标前缀已存在 gcc,跳过源码编译" else build_gcc touch "${STATE_DIR}/gcc.done" fi

标记文件只在成功最后一步写,失败时删掉标记,避免下次误判。回滚分两种:独立前缀可以直接删目录,系统目录不能用这招;包管理器安装用dnf history undo或apt-get remove,离线环境要先确认回滚包也在本地仓库里。脚本里把已安装文件记到install-manifest.txt,出问题至少知道动了哪些路径。

4. 常见排查:五类离线安装翻车现场与修复顺序

离线环境没有外网,报错信息就是黑匣子。排查顺序建议从包完整性、架构匹配、动态库路径、工具链优先级到事务残留,不要一上来就重装。下面五条是离线装 gcc 时最常遇到的翻车现场,每条按现象、原因、解决写。

4.1 现象:安装时报缺包,但清单里明明有

现象是dnf install报nothing provides ...,或者apt-get install报依赖不满足,但ls packages能看到对应包。原因通常有三个:本地仓库索引没生成或没刷新;包架构与目标机不匹配;包名和它提供的库名不是一回事,包管理器按provides找,不按文件名找。

先看架构和提供者:

uname -m rpm -qp --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' packages/rpm/*.rpm | sort dnf --disablerepo='*' --enablerepo='offline-gcc' repoquery --whatprovides 'libmpfr.so*'

rpm -qp查询未安装包的信息,repoquery --whatprovides查哪个包提供目标库。DEB 系用dpkg-deb -f 包名 Architecture和apt-cache policy看。解决方法是重新createrepo_c或dpkg-scanpackages,清掉包管理器缓存再试;架构不匹配就回收集机重新下对应架构的包,不要硬装。

4.2 现象:gcc -v 正常,编译报 cannot find -lstdc++

gcc -v能输出版本,但g++编译 C++ 报cannot find -lstdc++,或者报fatal error: bits/c++config.h: No such file。原因是只装了 gcc 没装 C++ 前端和标准库开发包,或者源码编译时--enable-languages=c,c++编了前端,但运行库路径没进链接器搜索路径。

先补包,再设路径:

export PREFIX=/opt/offline-gcc export PATH="$PREFIX/bin:$PATH" export LD_LIBRARY_PATH="$PREFIX/lib:$PREFIX/lib64:${LD_LIBRARY_PATH:-}" export LIBRARY_PATH="$PREFIX/lib:$PREFIX/lib64:${LIBRARY_PATH:-}" g++ -v -x c++ - -o /tmp/t <<'EOF' #include <iostream> int main() { std::cout << "ok\n"; } EOF

LIBRARY_PATH影响链接器找库,LD_LIBRARY_PATH影响运行时找库,两个都要设。-x c++ -表示从标准输入读 C++ 源码,适合现场快速验证。RPM 系补gcc-c++、libstdc++-devel,DEB 系补g++、libstdc++-dev。

4.3 现象:多版本 GCC 并存后系统工具链被带偏

装完新版本后,cc指向新版本,系统自带软件编译失败;或者gcc是新版但ld还是旧版。原因是PATH顺序变了,/usr/local/bin覆盖了系统路径,或者ldconfig缓存里混入了新库。离线环境里这种问题很难靠重装解决,因为一重装可能把系统包也带乱。

先看优先级和真实路径:

which -a gcc g++ ld readlink -f "$(command -v gcc)" ldd "$(command -v gcc)" | grep -E 'gmp|mpfr|mpc|isl' || true

which -a列出所有同名命令,readlink -f看真实路径,ldd看 gcc 自己链接了哪些库。解决方式是独立前缀加绝对路径调用,或者用update-alternatives管理但不要默认改系统。不要在/usr下make install,那是给自己找麻烦。

4.4 现象:脚本中断留下半装状态

脚本跑到一半断网、断电或者被 Ctrl-C,重跑提示已安装,但编译仍然失败;包管理器报锁被占用;源码目录里残留config.cache。原因是包管理器事务没完成,make install已经写了一部分文件,脚本又没有阶段标记。

先确认锁和残留:

STATE_DIR="${ROOT}/.state" mkdir -p "$STATE_DIR" if [[ -f "${STATE_DIR}/gcc.done" ]]; then echo "gcc 已编译完成,跳过" else build_gcc touch "${STATE_DIR}/gcc.done" fi

包管理器锁不要直接删,先用fuser或lsof确认没有安装进程。源码编译失败后删掉 build 目录重新 configure,比在残留目录上接着跑更稳。标记文件只在成功最后一步写,失败时删标记,避免下次误判。

4.5 现象:离线包在目标机报 GLIBC 版本符号缺失

现象是目标机运行 gcc 或编译出的程序报version 'GLIBC_...' not found。原因是收集包的机器系统版本高于目标机,包里的二进制依赖了目标机没有的 GLIBC 符号;或者源码编译时用了目标机不支持的 glibc 特性。这是离线交付里最难现场修的一类,因为换 glibc 风险很高。

先看符号和版本:

objdump -T "$(command -v gcc)" | grep GLIBC | sort -u | tail ldd --version | head -n 1

objdump -T看动态符号版本,ldd --version看目标机 glibc 版本。解决方式是在最低版本目标机同环境收集包,或者直接在目标机上源码编译。不要跨大版本拷二进制工具链,这是踩坑最多的做法。

5. 交付前校验:版本、ABI 与最小编译链路怎么验

装完不等于可用。离线包交付前要验证四件事:包完整、版本对、ABI 对、编译链路通。只看gcc --version不够,那只说明命令能跑,不说明 C++ 能编、库能找到、目标架构正确。

5.1 版本与 ABI 校验

先看目标三元组和搜索路径:

gcc -dumpmachine gcc -dumpversion gcc -print-search-dirs gcc -print-file-name=libstdc++.so file "$(gcc -print-prog-name=cc1)"

-dumpmachine输出目标三元组,应该和目标机架构一致;-dumpversion只输出版本主号;-print-search-dirs看库搜索

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进