锁文件里叫 child,打开却是 parent 锁文件里的名字还是child。归档的哈希也没变。可把这份文件重新读进来准备项目元数据读到的却是parent依赖也从child-dep换成了parent-dep。这不是一个包改了名字。它们是同一个 ZIP 里的两个项目一个在根目录一个在子目录。文件没有拿错文件里面的项目却选错了。这是十月最后一周里同一段导出路线上补的第三处。第一处是 PDM #395410 月 24 日提出25 日凌晨合并管入口——归档 URL 里的#subdirectory...能不能进到 requirement 里。第二处是 #395625 日提出27 日凌晨合并管读入端——锁文件里的归档哈希别在重新打开时丢掉上一篇《锁文件里明明有哈希PDM 为什么说没有》写的就是它。第三处是 PDM #3964。28 日提出29 日 12:04:27 批准12:04:38 合并中间隔了十一秒。生产代码两行。要看出这两行为什么省不掉得把已经修过的入口、一次成功的导出以及导出之后的重新读取按它们发生的顺序连起来看。日期都按 GitHub 记录的 UTC。一、入口这一站几天前刚修过归档里有三个项目根目录的parent子目录src/ab里的child另放一个src/a里的decoy。它们各自声明不同的依赖才看得出实际读到了谁普通目录的对照把同一个 child 放在src/child。要用的不是解压后随便哪个pyproject.toml而是明确指到子目录的那个child file:///.../bundle.zip#subdirectorysrc/a%26b这里的%26不是装饰。归档 requirement 的 fragment 由parse_qsl()读入先按拆成键值对再对每个值做百分号解码。目录名里的原样写进去就会在自己身上断成两条参数剩下的src/a恰好是归档里那个decoy的目录写成%26它才是数据。补的正是这一站的是 #3954。它让归档 requirement 从 URL fragment 里取出subdirectory再次生成链接时也带着它对于未显式命名、带子目录的归档不再先从外层文件名猜项目名而是把名字交给所选目录的元数据。再次生成链接这一环也要紧因为 requirements 导入不是把一行原文原封不动藏起来。导入函数会先解析再调用as_line()输出依赖。只在第一次解析时记得子目录下一次写出仍然可能丢掉。#3954 的回归测试已经把 parent、child、decoy 放进同一个归档检查选中的依赖来自 child普通 PDM 锁记录也带着子目录。入口这一站通了。可从「能读对」到「换成另一种锁文件格式以后还能读对」中间还隔着一道出口。二、导出成功为什么还不够反例是 #3964 自己记下的很具体先pdm add --no-sync再pdm export -f pylock导出记录仍叫 child把导出结果重新读入准备出来的元数据却来自 parent。命令没报错名字也没变。光是导出成功、name child这次往返看着就该算成功。问题在归档记录里。修复前的包记录长这样等价节选省掉 pylock 文件头与其他字段下载地址和哈希也做了缩写[[packages]] name child version 1.0 [packages.archive] url file:///.../bundle.zip [packages.archive.hashes] sha256 …名字有了文件有了哈希也有了唯独没说 child 在 ZIP 的哪里。内存里的 requirement 一直带着subdirectory。可父提交的 pylock 写出器的 archive 分支只写了下载 URL、可选路径和哈希——这个独立的属性没写出去。容易走岔的地方也在这里URL 里的 fragment 确实没了看着最直接的修法就是把#subdirectory...留下。但get_full_url()去掉 fragment并不是这次要推翻的选择。下载地址说的是取哪一个归档从归档里挑哪个项目pylock 有自己的archive.subdirectory字段。不能因为 URL 不再带选择器就让选择器从记录里彻底消失。丢掉的不是下载地址的一段尾巴而是一次格式转换没有接住的项目位置。三、哈希正确恰好无法替它作证上一篇的问题是文件里的哈希在读入时没有恢复。这次要看的两个版本都已经包含那个读入修复哈希完整保留子目录照样可能不见。几个 PR 不能各讲各的原因就在这——它们接通的是同一条路线上的不同站。修复接通了哪一段不能代替什么#3954归档 URL 中的子目录进入 requirement生成链接和普通 PDM 锁记录时继续保留不负责 pylock archive 分支的写出#3956读取 pylock 时把 archive 哈希恢复到 candidate不负责保存项目目录#3964写 pylock 时把 requirement 的子目录写进 archive 表不重做前两项已经接通的入口和哈希读取这层关系是 #3964 原文自己交代的其中一句“Neither wrotearchive.subdirectoryin the pylock converter.”前两项都没有在 pylock 转换器里写出这个字段。不是同一个补丁反复没修好是同一份信息换一种表示每一站都得有人接手。一个 ZIP 完全可以包含多个pyproject.toml。选 parent、选 child、选 decoy归档字节都可以一模一样。SHA-256 只能核对拿到的是否仍是这份归档补不出「进哪个目录」这个选择。所以哈希相同不是这个反例里的矛盾反倒是最要紧的条件文件一直没换过换掉的是解释它时站在哪个目录。四、两行补丁接回的不是包名最终的修复加在 archive 分支ifreq.subdirectory:archive[subdirectory]req.subdirectory有子目录就写没有就不加字段。同一个函数里VCS 和 directory 两个分支早就这么留着自己的子目录archive 缺的只是这一步。写出去的值是实际目录src/ab不是 URL fragment 中的src/a%26b。URL 编码属于把选择器放进链接时的表示到了结构化字段里该存的是解码后的路径。pylock 对 archive 子目录的定义转引了 VCS 子目录规则它指出源码树中项目根的位置。补上以后字段落在packages.archive表里和 URL、hashes 平级[packages.archive] url file:///.../bundle.zip subdirectory src/ab # hashes 沿用原值包名不用改。哈希不用重算下载 URL 也不用把 fragment 拼回去。要恢复的是下次打开这份归档时进哪个目录。pylock 读入器本来就会读archive.subdirectory把它交回 requirement。不是 reader 不懂这个字段是 writer 根本没给它写。随后as_file_link()把子目录重新放进安装链接_unpack()交给解包器选目录prepare_metadata()再从实际选中的路径读项目元数据。这才解释了开头那个看起来不可能的组合记录里写着 child元数据却来自 parent。名字来自锁文件元数据来自后来真正打开的那个目录——两者不是同一次读取里的同一个事实。五、换一次全新的读取看它究竟打开谁复现放在 subdirectory_demo固定 #3964 的实际父提交与合并提交。两边都已经有 #3954 和 #3956所以差出来的那一点只能归到这次补的段上。从同一份本地 ZIP 出发用两份完整 PDM 源码分别跑 requirements 导入、候选元数据读取和 pylock 转换序列化出来的 TOML 再交给新的LockedRepository看新 candidate 准备出来的元数据。写出前还记得子目录的旧对象不算读回结果。这次直接调用真实的导入器、转换器与读入器省去完整 add 命令的依赖求解parent-dep这些名字只当元数据观察值不去下载安装。输入分 URL 与本地路径两类URL 场景由进程内 transport 提供同一份 ZIP 字节不访问公网执行期间禁止创建网络 socket。两类来源各跑不指定目录、普通目录、含目录三种场景修前修后共 12 次重新读取元数据另加两版 decoy 对照。URL / path 两组结果相同合并展示完整输出与环境见 工程 README。指定的子目录修复前重新准备的项目修复后重新准备的项目不指定parentparentsrc/childparentchildsrc/abparentchild两个指定子目录的场景两边的导出记录都叫 child归档哈希也相同差的是修复前没有archive.subdirectory修复后原来的目录还在。含的那组还得选中完整目录src/ab不是诱饵src/a。上游合并的回归测试从 add、export 一路检查到新读回 candidate 的子目录属性本文的复现多走一步到元数据为止观察 parent-dep 与 child-dep。准备元数据时上游调用的是_unpack(validate_hashesFalse)实验另外算 ZIP 的 SHA-256核对导出和读回的哈希字段始终相同看到元数据就停不执行构建安装。六、六天、三处和最后那一站10 月 24 日到 29 日这条路上补了三处入口、读入端的哈希、写出端的目录。每次改动都不大——这次两行——大的是它们在一条链上各自负责的那一段。只看两行补丁最容易把前后最值钱的部分压掉入口已经修过哈希也已经修过导出照样能成功地把另一项信息丢掉。每一站单看都正常重新打开归档才发现拿到的已不是当初选的那个项目。往返保真的对象不只是文件里的字符串还包括下一次操作究竟会选中什么。上一篇补的是读入端文件里有的信息读回来时要恢复。这一篇补的是写出端内存里知道的写出去时要留下。两头都接住锁文件才不只是看起来还像原来那份。源码与复现主修复PDM #3964前序关联 #3954、#3956。合并日期UTC#3954 2026-10-25、#3956 2026-10-27、#3964 2026-10-29。修复前7aaeb99最终合并f01b858。离线对照与来源校验subdirectory_demo。本文观察到重新准备项目元数据为止未执行包安装。