根本原因不是版本冲突,而是Composer 2.x默认canonical: true导致私有仓库阻断Packagist查找;需显式设"canonical": false启用多源回退,并验证私有源packages.json是否包含所需版本元数据。

根本不是版本冲突,而是私有仓库把 Packagist 挡住了——canonical: true(默认值)让 Composer 一找到同名包就停止搜索,哪怕那个包不满足你的版本约束。
为什么 composer.json 配了私有源,却装了旧版包?
Composer 2.x 默认对每个仓库启用 canonical: true。这意味着:只要私有源返回了 vendor/package 的元数据(哪怕只有 v1.0),Composer 就不再查 Packagist,哪怕你写的是 "^2.0",也只会从私有源里挑一个“能凑合”的版本装,甚至报错说“no matching package found”。
- 验证方式:
composer show vendor/package看source字段;composer repo:list看仓库声明顺序 - 私有源必须实际提供你所需版本的
packages.json片段(含dist和source地址可访问) - 如果私有源只托管部分包(比如仅
myorg/utils),就必须显式设"canonical": false
canonical: false 怎么配才生效?
这个配置不能全局开关,必须写在每个需要“非阻断”的仓库定义里:
{
"repositories": [
{
"type": "composer",
"url": "https://my-priv.repo",
"canonical": false
},
{
"type": "composer",
"url": "https://repo.packagist.org"
}
]
}
- 多个
"canonical": false仓库按声明顺序依次查询,第一个返回匹配结果的胜出 - 两个非规范仓库都提供
monolog/monolog的2.10.0,Composer 取前面那个,不会合并版本列表 - 别漏掉
"type": "composer"—— VCS 类型仓库(如"type": "vcs")不受canonical控制
怎么验证新配置真起作用了?
别等下次 composer update 出问题再排查,用这几个命令快速确认:
- 运行
composer show vendor/package,确认source指向你预期的仓库(比如 Packagist 而不是私有源) - 执行
composer update --dry-run -v,观察日志里是否出现多源查询痕迹(如Reading ... from https://my-priv.repo→Reading ... from https://repo.packagist.org) - 临时绕过锁文件验证兼容性:
composer update --with=vendor/package:^2.0,看是否能成功解析并拉取新版
真正容易被忽略的是:即使 "canonical": false 配对了,如果私有源返回的 packages.json 里压根没包含你要的版本号(比如漏同步、或构建脚本没生成对应 dist),Composer 还是会跳过它继续往后查——但很多人误以为“配了就完事”,其实得双向验证源端数据完整性。











