朕喜技术(ZhenxiJS)
2026.09.27
1 分钟阅读

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。

六、经验与避坑

  1. cargo source replacement 是单数 [source.*]。 [sources.*] 会被静默忽略,零提示,报错却指向 crates-io,因果完全脱节。 遇到 location searched: registry crates-io 而离线镜像里明明有包时, 第一件事就是 grep -n '^\[source' .cargo/config.toml 看段落名。
  2. replace-with 的目标要能离线解析:directory 源(指向已解包目录) ✅;[registries.*] + file-dir(需要 index 缓存)❌。 file-dir 不是「包目录」,别被配置里的注释带偏。
  3. directory 源必须带 .cargo-checksum.json,每个包目录一份。 漏了它的报错和段落名写错完全一样,必须用最小化对照实验区分。
  4. *.crate 是 tarball,目录源要解包;两者不能混用同一个路径语义。
  5. 警惕「改号包」:1.0.4-patched.1 这类 semver 预发布后缀, ^1.0.4 匹配不到;离线镜像同步时必须归一化版本号,否则口径全乱。
  6. "1.0.3" 是 ^1.0.3,不是「等于 1.0.3」。要精确锁版本写 "=1.0.3" 或提交 Cargo.lock。不提交 lock 时,镜像里一旦出现更高的兼容版本就会静默漂移。
  7. 修复要落到「被验证的那个容器」里:/srv/vendor 是容器内新建目录, docker compose run --rm <svc> 起的是新容器,不会复用先前容器的产物, 需要重新执行修复脚本。
  8. bind mount 会让「修复看起来生效」:app/ 是挂载目录,改动会写回宿主; 而 /srv/vendor 在容器里 —— 换容器就没了,别把两者混为一谈。
  9. 自检脚本的外部依赖也要装齐:verify.sh 用 python3 -c "import tomllib…", 镜像缺 python3 会让该断言恒红,与真实问题无关,容易带偏排查。
  10. 同类影响面:任何「配置段落名/键名拼错但被静默忽略」的场景 (YAML 缩进、~/.cargo/config.toml 优先级、pip.conf、.npmrc、 settings.xml mirror 配置),现象都是「配置看起来没问题、行为却按默认值走」。

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-27-0000-cargo-sourcesources

分享这篇文章