cargo 离线构建报 no matching package named xxx found?先查 source replacement 的段落名写成了 sources 还是 source
日期:2026-09-27 | 环境:Docker +
rust:1.79-slim+cargo 1.79.0+ 内网离线镜像源(无外网) 关键词:cargo fetch --offline 失败、no matching package named found、location searched registry crates-io、 source replacement 不生效、[source.crates-io]vs[sources.crates-io]、directory source、.cargo-checksum.json、cargo vendor 离线依赖、内网私有 registry、file-dir、sparse index、 Rust 内网构建机依赖解析、CI 构建卡在 cargo fetch
一、问题背景
内网构建机(无外网,network_mode: none)构建一个 Rust 服务 gateway-lite。
依赖全部来自内网离线镜像源 /srv/vendor-mirror(目录里就是 *.crate 原文件)。
构建第一步 cargo fetch --offline 就失败:
error: no matching package named `secure-token` found
location searched: registry `crates-io`
required by package `gateway-lite v0.1.0 (/srv/app)`
cargo fetch 退出码 = 101
诡异之处:
- 镜像目录里明明有
secure-token-1.0.3.crate Cargo.toml里也写了secure-token = "1.0.3"- 报错却说「在 registry
crates-io里搜不到这个包」——它根本没去离线镜像源里找
配置文件长这样(app/.cargo/config.toml):
[sources.crates-io] # ← 注意这里是复数 sources
replace-with = "inner-mirror"
[registries.inner-mirror]
index = "sparse+http://10.10.20.31:9000/index/"
registries.inner-mirror.config = { file-dir = "/srv/mirror" }
二、复现步骤
# 起环境(builder 有网、builder-offline 真无外网)
docker compose up -d --build
docker compose exec builder bash
# 复现
/srv/ops/repro.sh
# -> error: no matching package named `secure-token` found, 退出码 101
# 问题态自检基线
/srv/ops/verify.sh
# -> PASS=7 FAIL=5 / RESULT: BAD
最小化对照实验(两个配置只差一个字母):
# 实验 A:复数 sources —— 失败
cat > .cargo/config.toml <<'CFG'
[sources.crates-io]
replace-with = "inner-mirror"
[source.inner-mirror]
directory = "/srv/vendor"
CFG
cargo fetch --offline # -> error: no matching package named `secure-token` found, RC=101
# 实验 B:单数 source —— 成功
cat > .cargo/config.toml <<'CFG'
[source.crates-io]
replace-with = "inner-mirror"
[source.inner-mirror]
directory = "/srv/vendor"
CFG
cargo fetch --offline # -> Locking 2 packages to latest compatible versions, RC=0
三、根本原因
一共 4 个独立缺陷,必须同时修掉:
1. 段落名写错:[sources.crates-io] 应为 [source.crates-io]
cargo 的 source replacement 配置段是单数 [source.*]。
写成复数 [sources.*] 时,cargo 静默忽略整段 —— 不报错、不告警、没有 warning。
于是 crates-io 从未被替换,cargo 老老实实去查 crates-io,
在离线模式下当然查不到 → no matching package named XXX found。
这是「报错信息与真实原因完全脱节」的典型:错误说「registry 里没这个包」, 而真相是「你压根没配置好替换源」。
2. 替换目标类型错:要用 directory 源,不是 [registries.*]
即使段落名改对,把 replace-with 指向 [registries.inner-mirror](sparse+… + file-dir)
仍然失败。原因:
- registry 源在离线时没有 index 缓存,解析不到任何包
file-dir是 registry 源的 cache 目录语义,不是「包目录」- 这里指向的
/srv/mirror根本不存在
正确做法是 [source.X] 用 directory= 指向已解包的目录。注意 *.crate 只是
tarball,必须展开成目录,且每个包目录要有 .cargo-checksum.json:
tar -xzf secure-token-1.0.3.crate -C /srv/vendor
cd /srv/vendor/secure-token-1.0.3
printf '{"files":{"Cargo.toml":"%s","src/lib.rs":"%s"},"package":null}\n' \
"$(sha256sum Cargo.toml | awk '{print $1}')" \
"$(sha256sum src/lib.rs | awk '{print $1}')" > .cargo-checksum.json
漏了 .cargo-checksum.json 的报错和缺陷 1 一模一样,非常容易误判。
3. 镜像里混进了「改号包」-patched.1
镜像里有一个 secure-token-1.0.4-patched.1.crate,用 semver 预发布后缀伪装成
「安全修复包」。由于 1.0.4-patched.1 < 1.0.4,^1.0.4 匹配不到它:
error: failed to select a version for the requirement `secure-token = "^1.0.4"`
candidate versions found which didn't match: 1.0.4-patched.1, 1.0.3, 1.0.2, ...
if you are looking for the prerelease package it needs to be specified explicitly
必须把这种包归一化成正规版本号(重打包为 secure-token-1.0.4.crate),口径才统一。
4. ^1.0.3 会漂移到 1.0.4
secure-token = "1.0.3" 的 cargo 语义是 ^1.0.3,会解析到镜像里更高的 1.0.4,
导致 Cargo.lock 版本与 Cargo.toml 声明版本不一致。需要提交并保留 Cargo.lock
把版本钉死。
四、解决办法
ops/fix.sh 四步:
MIRROR=/srv/vendor-mirror
VENDOR=/srv/vendor
APP=/srv/app
# 1) 归一化改号包:1.0.4-patched.1 -> 1.0.4
work=$(mktemp -d)
tar -xzf "$MIRROR/secure-token-1.0.4-patched.1.crate" -C "$work"
sed -i 's/^version = "1\.0\.4-patched\.1"/version = "1.0.4"/' \
"$work/secure-token-1.0.4-patched.1/Cargo.toml"
mv "$work/secure-token-1.0.4-patched.1" "$work/secure-token-1.0.4"
( cd "$work" && tar -czf "$MIRROR/secure-token-1.0.4.crate" secure-token-1.0.4 )
rm -rf "$work" "$MIRROR/secure-token-1.0.4-patched.1.crate"
# 2) 展开为 directory source,并生成 .cargo-checksum.json
rm -rf "$VENDOR"; mkdir -p "$VENDOR"
for f in "$MIRROR"/secure-token-*.crate; do tar -xzf "$f" -C "$VENDOR"; done
for d in "$VENDOR"/*/; do
d=${d%/}
printf '{"files":{"Cargo.toml":"%s","src/lib.rs":"%s"},"package":null}\n' \
"$(sha256sum "$d/Cargo.toml" | awk '{print $1}')" \
"$(sha256sum "$d/src/lib.rs" | awk '{print $1}')" > "$d/.cargo-checksum.json"
done
# 3) 修正段落名:必须是单数 [source.crates-io] / [source.inner-mirror]
cat > "$APP/.cargo/config.toml" <<'CFG'
[source.crates-io]
replace-with = "inner-mirror"
[source.inner-mirror]
directory = "/srv/vendor"
CFG
# 4) 生成并保留 Cargo.lock,钉住声明版本
cd "$APP"
want=$(awk -F'"' '/^secure-token/{print $2; exit}' Cargo.toml)
cp Cargo.toml /tmp/bak
sed -i "s/^secure-token = \"${want}\"/secure-token = \"=${want}\"/" Cargo.toml
cargo fetch --offline # 生成 lock
cp /tmp/bak Cargo.toml # 改回原声明
cargo fetch --offline # lock 稳定停在 1.0.3
Dockerfile 另外补了 python3(自检脚本 A4 用 python3 -c "import tomllib…" 校验配置,
镜像里没装会导致该断言恒 FAIL,与问题本身无关,容易误导排查方向)。
五、验证
# 问题态
/srv/ops/verify.sh -> PASS=7 FAIL=5 / RESULT: BAD
# 修复态
/srv/ops/repro.sh -> cargo fetch 退出码 = 0 / RESULT: OK
/srv/ops/verify.sh -> PASS=12 FAIL=0 / RESULT: OK
python3 /srv/client/check_build.py
# declared_version = 1.0.3
# cargo_fetch_rc = 0
# lock_version = 1.0.3
# cargo_build_rc = 0
# RESULT: OK
# 真的编译 + 运行
/srv/app/target/debug/gateway-lite
# token=st.gateway-lite|read:config|0.21556a7dcc17a26c
# 真·无外网容器(network_mode: none)
docker compose run --rm builder-offline bash -c '/srv/ops/fix.sh >/dev/null && /srv/ops/verify.sh'
# PASS=12 FAIL=0 / RESULT: OK
verify.sh B 组 6 条判据全绿:cargo fetch 退出码 0、Cargo.lock 版本 = 声明版本、
cargo tree 解出 secure-token v1.0.3、cargo build --offline 成功、
镜像里无改号包、坏输入(未同步的 beacon crate)仍能正确报 no matching package。
六、经验与避坑
- cargo source replacement 是单数
[source.*]。[sources.*]会被静默忽略,零提示,报错却指向crates-io,因果完全脱节。 遇到location searched: registry crates-io而离线镜像里明明有包时, 第一件事就是grep -n '^\[source' .cargo/config.toml看段落名。 replace-with的目标要能离线解析:directory源(指向已解包目录) ✅;[registries.*]+file-dir(需要 index 缓存)❌。file-dir不是「包目录」,别被配置里的注释带偏。directory源必须带.cargo-checksum.json,每个包目录一份。 漏了它的报错和段落名写错完全一样,必须用最小化对照实验区分。*.crate是 tarball,目录源要解包;两者不能混用同一个路径语义。- 警惕「改号包」:
1.0.4-patched.1这类 semver 预发布后缀,^1.0.4匹配不到;离线镜像同步时必须归一化版本号,否则口径全乱。 "1.0.3"是^1.0.3,不是「等于 1.0.3」。要精确锁版本写"=1.0.3"或提交Cargo.lock。不提交 lock 时,镜像里一旦出现更高的兼容版本就会静默漂移。- 修复要落到「被验证的那个容器」里:
/srv/vendor是容器内新建目录,docker compose run --rm <svc>起的是新容器,不会复用先前容器的产物, 需要重新执行修复脚本。 - bind mount 会让「修复看起来生效」:
app/是挂载目录,改动会写回宿主; 而/srv/vendor在容器里 —— 换容器就没了,别把两者混为一谈。 - 自检脚本的外部依赖也要装齐:
verify.sh用python3 -c "import tomllib…", 镜像缺python3会让该断言恒红,与真实问题无关,容易带偏排查。 - 同类影响面:任何「配置段落名/键名拼错但被静默忽略」的场景
(YAML 缩进、
~/.cargo/config.toml优先级、pip.conf、.npmrc、settings.xmlmirror 配置),现象都是「配置看起来没问题、行为却按默认值走」。
