
pnpm 全局安装容错升级add -g/update -g如何安全忽略不完整的外部包组【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm导读当你在全局环境中执行pnpm add -g或pnpm update -g时pnpm 需要先扫描全局包目录、读取每个包组一组共享同一份安装清单的全局依赖的清单再决定如何接入新的命令。如果一个不相关的全局包组因清单损坏、缺失而只被读出了一半旧版本 pnpm 可能据此误删或误改命令而本次变更见 .changeset/fix-global-retained-bin-scan.md让这类会被完整替换的操作在被替换包组的每个命令都被保留的前提下可以放心跳过这些不完整的外部包组。读完本文你将理解 pnpm 全局安装目录的物理结构、命令所有权bin ownership判定逻辑、保留命令集合retained bin names如何参与保护以及可忽略不完整包组与必须 fail-closed两种路径的边界。一、变更本身一句话看懂本次 changeset 声明如下pnpm/global.commands: patch、pnpm/bins.linker: patch、pnpm: patch、pacquet: patchpnpm add -gandpnpm update -gnow ignore incomplete unrelated global package groups when every command from the replaced group is retained. Operations that could remove a global command still require complete ownership information.翻译成可操作的行为描述可以跳过pnpm add -g/pnpm update -g过程中如果某个与被替换包组无关的包组无法被完整读取清单缺失、损坏、不可读只要被替换包组的全部命令都会被新安装保留下来即不涉及删除任何命令pnpm 就不再因为读不完整而报错而是忽略该不完整包组继续完成安装仍然严格任何可能删除全局命令的操作例如pnpm remove -g或更新时命令集合会缩水的情形仍然要求完整的所有权信息读不完整就 fail-closed绝不半途而废地动 bin 目录。二、背景全局安装的目录模型与包组概念要理解这次修复先看 pnpm 全局安装在磁盘上的物理结构相关实现见 pnpm11/global/packages/src/scanGlobalPackages.ts全局包目录globalDir默认是 pnpm 的全局目录内部存放两种条目每个安装目录install dir一个真实目录里面有一份package.json记录该组全局依赖及其版本范围和node_modules/每个哈希链接hash link一个以内容哈希命名的符号链接指向某个安装目录。getHashLink/createInstallDir的实现见 pnpm11/global/packages/src/globalPackageDir.ts。包组package group由同一次pnpm add -g产生的一组依赖共用一份package.json。scanGlobalPackages遍历globalDir对每个哈希链接realpath后读取其package.json再对dependencies里的别名做isValidGlobalDependencyAlias校验从而得到GlobalPackageInfo[]含hash、installDir、dependencies。全局 bin 目录globalBinDir存放命令 shim如pnpm、eslint、tsc等。shim 指向的并非某个安装目录本身而是指向包组哈希链接的路径参见 pnpm11/global/commands/src/globalActivation.ts 中hashLinkedPkgs的注释shim 内嵌了生成它的目标路径因此只要 shim 指向哈希链接下一次更新移动哈希链接即可无需重写全部 shim。读取包组时的不完整从何而来scanGlobalPackages在扫描时会静默跳过读不到package.json的哈希链接catch { continue }getGlobalPackageDetails使用safeReadPackageJsonFromDir逐个读取子包清单。也就是说一个包组只要有一份子包 manifest 读不出来它在扫描结果里就是不完整的。旧逻辑中任何操作只要需要这些信息去判定所有权就会被这个不完整组卡住见下文对照的 fail-closed 变更 .changeset/fail-closed-global-bin-enumeration.md。三、核心机制保留命令集合retainedBinNames如何让扫描放宽3.1 所有权判定的入口所有权判定的核心函数位于 pnpm11/global/commands/src/binOwnership.tsexport async function getGlobalBinOwnership ( globalDir: string, targetGroups: GlobalPackageInfo[], retainedBinNames: Setstring ): Promise{ groups: GlobalPackageBinSnapshot[], protectedBins: Setstring } { const targetHashes new Set(targetGroups.map(({ hash }) hash)) const targetBinNames await Promise.all(targetGroups.map((pkg) getInstalledBinNames(pkg))) const groups targetGroups.map((info, index) ({ info, binNames: targetBinNames[index] })) const binNamesToProtect new Set(targetBinNames.flat().filter((name) !retainedBinNames.has(name))) if (binNamesToProtect.size 0) return { groups, protectedBins: new Set() } const survivingGroups scanGlobalPackages(globalDir).filter((pkg) !targetHashes.has(pkg.hash)) const survivorBinNames await Promise.all(survivingGroups.map((pkg) getInstalledBinNames(pkg))) const protectedBins new Set( survivorBinNames.flat().filter((name) binNamesToProtect.has(name)) ) return { groups, protectedBins } }其中注释点明了本次变更的设计意图见 binOwnership.ts每次 manifest 读取都在调用方改动全局状态之前 settle这让不完整的目标能够 fail-closed仅当某个目标 bin 不会被保留时才需要去检查幸存的包组——因为除此之外没有任何其他 bin 会被删除。3.2retainedBinNames从哪里来在pnpm update -g的流程pnpm11/global/commands/src/globalUpdate.ts中更新单个包组的步骤是用createInstallDir创建新安装目录installGroup把解析结果写入新目录的package.json与 lockfilereadInstalledPackages(installDir)读取新安装的包组见 pnpm11/global/commands/src/readInstalledPackages.ts它用readPackageJsonFromDirRawSync读组清单、用readPackageJsonFromDir逐个读node_modules/alias下的子包 manifestcheckGlobalBinConflicts算出与存量包组的冲突并得到binsToSkipgetActualBinNames({ pkgs, binsToSkip })计算出新安装实际会提供哪些命令这就是retainedBinNames在 globalActivation.ts 中由getActualBins得到解析出所有要链接的 bin再过滤掉目标文件不存在的项拿着retainedBinNames调用getGlobalBinOwnership(globalDir, [pkg], retainedBinNames)。关键在第 4 步之后只要新包组提供的命令集合 ⊇ 旧包组提供的命令集合也就是binNamesToProtect.size 0函数会立刻返回protectedBins: new Set()完全不去扫描幸存的包组——这正是每个被替换包组的命令都被保留时可以忽略不完整外部包组的源码落点。3.3 什么情况下仍必须完整读取一旦retainedBinNames不完整即旧包组的某些命令不会出现在新安装中binNamesToProtect非空就必须scanGlobalPackages(globalDir)遍历所有幸存包组对每个幸存包组执行getInstalledBinNames内部逐个readPackageJsonFromDir读不出来会抛错找出幸存包组里也提供同名命令的集合作为protectedBins防止removeBin误删别的全局包的命令见 globalRemove.ts 中与幸存包组共享的 bin 不得 unlink的注释。如果幸存包组此时读不完整异常会直接向上抛激活流程进入cleanupFailedGlobalInstall现有全局安装保持原样——这是 .changeset/fail-closed-global-bin-enumeration.md 确立的fail-closed原则pnpm add -g、pnpm update -g、pnpm remove -g都不能在只部分读取包组后去改动全局 bin 或安装目录对应 issue pnpm/pnpm#13796。四、两条路径的完整调用链对比场景是否扫描幸存包组遇到不完整外部包组时的行为依据add -g/update -g新命令集合保留旧组全部命令否binNamesToProtect.size 0提前返回忽略正常完成激活binOwnership.tsadd -g/update -g有命令会被替换掉是读不完整即抛错 → 回滚/清理旧安装保留binOwnership.ts、globalActivation.tsremove -g是retainedBinNames为空集任何 bin 都可能被删必须完整所有权信息globalRemove.ts激活阶段如何保证忽略不破坏原子性即使放宽了所有权扫描activateGlobalInstall的原子切换逻辑并不放松globalActivation.tsprepareGlobalInstall解析实际 bin、创建.pnpm-bin-backup-*备份目录、备份现有 bin 槽位、记录旧哈希链接目标swapHashLink在非 Windows 上先把新链接写到${hashLink}.${process.pid}.tmp再rename覆盖旧哈希链接——一次移动完成切换并发命令永远不会观察到链接缺失Windows 因无法覆盖 junction采用非原子的symlinkDir覆盖linkBinsOfPackages只为目标真正变化的命令补写 shim对更新同一组命令而言shim 数量为零失败时restoreGlobalInstall恢复备份的 bin 槽位与旧哈希链接cleanupFailedGlobalActivation清理残留。结合 .changeset/atomic-global-install-activation.md 可知全局安装的 shim 始终指向稳定的按包组哈希链接而非具体安装目录因此更新版本只移动一个链接、不再逐 shim 重写安装进行中命令也不会从PATH中消失。本次忽略不完整外部包组的修复正是在这一原子框架内把所有权判定的读盘范围收窄到确实需要保护命令时。五、测试与回归保障仓库在 pnpm11/global/commands/test 下提供了配套测试globalAdd.test.ts验证add -g在存在不完整/无关包组时能正常完成globalUpdate.test.ts覆盖全部命令保留时忽略不完整组与可能删命令时要求完整信息两种分支globalRemove.test.ts 与 removeBinWindows.test.ts确保删除路径始终 fail-closed且 Windows 上.cmd/.ps1/.exe多扩展名槽位见 globalActivation.ts 的扩展名列表处理正确getInstalledBinNames.test.ts直接验证包组 bin 枚举的完整性判定。六、变更的适用前提与边界适用版本范围本变更同时作用于 TypeScript 实现pnpm11/目录与 Rust 重实现pacquet属于 pnpm 主版本线内的 patch 级行为修正不改变任何命令行参数或配置项不改变安全底线忽略只发生在add/update 且命令全部保留的场景凡涉及删除命令remove、命令集合收缩的 update、清理被替换包组仍严格 fail-closed保证任何命令都不会在安装进行中从PATH缺失对用户的实际收益全局目录中某个无关包组因手动改动、权限问题或磁盘异常而 manifest 不可读时日常add -g/update -g不再被意外阻断而涉及删除的操作依然宁可失败也不冒险删错命令。七、小结本次 changeset 用一行行为描述背后串联起 pnpm 全局安装的完整所有权模型包组哈希链接的目录结构scanGlobalPackages.ts、保留命令集合驱动的扫描剪枝binOwnership.ts、以及原子激活与回滚框架globalActivation.ts。理解何时可以忽略不完整包组、何时必须完整所有权信息这两条边界就能准确预判 pnpm 全局命令在异常磁盘状态下会采取的行为——这也是在故障环境中安全运维全局包管理器的重要心智模型。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考