离线安装失败的根本原因是composer不校验包内容一致性,仅依赖lock文件和installed.json;必须通过composer archive生成带哈希的依赖快照,并用--repository-url指向本地目录实现严格复现。

composer install 为什么离线会失败?
离线安装失败,根本原因不是没网,而是 composer install 默认只读 vendor/composer/installed.json 和锁文件 composer.lock,但不会校验包内容是否真和在线时一致——比如哈希不匹配、压缩包被篡改、或本地缓存混入了不同版本的 dist 包。
常见错误现象:installing myvendor/mylib (1.2.3): Downloading (failed)(离线时还试图下载),或更隐蔽的 Package operations: 1 install, 0 updates, 0 removals 看似成功,实际装的是旧版/损坏包。
- 必须确保
composer.lock是从可信在线环境生成的(即由composer update或首次composer install产出) - 离线机器不能有残留的
vendor/或~/.composer/cache/中冲突的包缓存 -
composer install --no-scripts --no-plugins可跳过运行时干扰,但不解决包一致性本身
如何让离线环境严格复现在线安装结果?
核心动作是「冻结完整依赖快照」:把在线环境里真正下载下来的每个包(含 dist zip/tar 哈希、版本、URL)打包带走,离线时强制 Composer 只从这个快照取包,不查网络、不走缓存。
实操分三步:
- 在线机器执行:
composer install --no-dev --prefer-dist(确保用 dist 模式,生成标准composer.lock) - 运行:
composer archive --format=zip --file=deps.zip --without-dev(它会按composer.lock打出所有依赖源码 zip,含精确版本与哈希) - 离线机器解压 deps.zip 到临时目录,再执行:
composer install --no-dev --repository-url=file:///path/to/deps/unpacked(注意路径必须是绝对路径 +file://协议)
关键点:--repository-url 指向的是一个本地目录,Composer 会把它当私有仓库扫描,只加载其中符合 composer.lock 版本约束的包,跳过 Packagist 查询。
为什么不用 vendor 目录直接拷贝?
直接复制 vendor/ 看似最简单,但实际踩坑最多:Composer 不保证 vendor/ 内部结构可移植;autoload_files.php 里可能含绝对路径;某些包在 install 时执行的 post-install-cmd 脚本(如生成 classmap、编译扩展)没运行;还有 symlink 问题(Windows/Linux 跨平台失效)。
更严重的是,vendor/ 里没有记录「这个包当时是从哪个 URL 下载的」「SHA256 是多少」,下次 composer update 或 require 新包时,Composer 会重新联网校验甚至覆盖已有包。
- 拷过去后运行
composer dump-autoload只能补 autoloader,不修复包来源可信性 - 若项目用了
path类型仓库(本地开发包),拷vendor/后路径指向会断 -
composer show --tree在离线机上仍可能报错,因 metadata 缺失
离线安装后如何验证一致性?
装完不是终点,得确认「和线上一模一样」。最可靠方式是比对 lock 文件声明的哈希与实际安装包的哈希是否吻合。
手动验证太重,推荐用这个轻量检查:
- 运行:
composer show --locked --format=json | jq -r '.[] | "\(.name) \(.dist.reference)"' | sort > online-hashes.txt(在线机导出) - 离线机执行相同命令,输出存为
offline-hashes.txt -
diff online-hashes.txt offline-hashes.txt—— 零输出才代表完全一致
注意:.dist.reference 是 dist 包的 commit hash 或 zip 文件 SHA256,它是 Composer 锁定真实内容的唯一依据。只要这个值对得上,哪怕包名版本号看起来一样,也能排除「同名不同源」的风险。
最容易被忽略的是:composer.lock 里的 dist 字段可能为空(比如用了 --prefer-source),此时必须确保离线环境也强制走 source 模式,否则 fallback 到 dist 会触发下载。










