conda 内置 Windows Launcher 兼容机制解析:从 cli-32.exe/cli-64.exe 到 conda-launchers 包的演进
2026/9/16 12:14:25 网站建设 项目流程

conda 内置 Windows Launcher 兼容机制解析:从 cli-32.exe/cli-64.exe 到 conda-launchers 包的演进

【免费下载链接】condaA system-level, binary package and environment manager running on all major operating systems and platforms.项目地址: https://gitcode.com/GitHub_Trending/co/conda

conda 的 Windows 入口点(entry point)在 Windows 上以.exe可执行文件形式存在,本文围绕 conda/shell/README.md 中记录的cli-32.execli-64.exe两个内置 stub 启动器,剖析它们在 conda 从「自带 stub」过渡到「使用独立conda-launchers包」过程中的兼容性角色、源码级实现原理与移除约束。读完本文,你将理解 conda 在 Windows 上创建入口点 .exe 的完整选择链路(含自升级场景的时序问题),掌握conda-launchers的查找、哈希校验与回退逻辑,以及为何这两个内置文件至今没有明确的移除时间表。

背景:为什么 conda/shell 下躺着两个 Windows 可执行文件

在 Windows 平台,conda 创建 Python 包入口点(如conda.exepip.exe)时,并不是直接复制 Python 脚本,而是复制一个「启动器 stub」可执行文件:它负责定位解释器并启动对应的 Python 模块。过去,conda 依赖安装在Scripts/conda.exe的自身副本;而从 conda 26.3.0 开始(见 CHANGELOG.md 中收录的 PR #15678),conda 改为使用内置的 Windows stub 可执行文件来创建入口点,并引入平台到 stub 的映射。

当前 conda/shell/ 目录中保留了两个文件:

文件角色
conda/shell/cli-64.exe64 位 Windows 入口点 stub
conda/shell/cli-32.exe32 位 Windows 入口点 stub(回退用)

其余如 conda/shell/condabin/conda.bat、conda/shell/etc/profile.d/conda.sh 等则是激活/反激活脚本,与本文讨论的入口点 stub 属于不同机制,不在此展开。

需要说明的是,这两个文件仅是「为兼容性保留」的过渡产物。conda 现在优先使用独立的conda-launchers包(recipe/meta.yaml中以conda-launchers >=24.7.1 # [win]声明为 Windows 运行时依赖)来提供入口点,详见后文。

cli-64.exe:自升级事务的「安全垫」

conda/shell/README.md 明确指出:包括 26.7.2 在内的已发布 conda 客户端,在创建 Windows 入口点时仍会复制cli-64.exe。这里的关键在于自升级(self-upgrade)的时序问题

  1. 用户运行conda update conda,触发对 conda 自身的更新事务;
  2. 事务会替换 conda 的包文件(包括旧的启动器文件);
  3. 正在运行的进程(当前执行的 conda 进程)使用的仍是内存中已加载的旧代码,它不会因为磁盘文件被替换而切换到新代码;
  4. 如果新包中不再包含旧的启动器文件,那么「运行中的旧代码 + 已被替换的磁盘文件」组合下,事务将无法找到需要的启动器而失败。

因此,把cli-64.exe保留在新发布的包中,就是为了让这些正在执行的旧版本 conda 进程能够完成其升级事务。这正是 conda/core/launchers.py 中注释「Prefer the extracted package during upgrades, before installed files are unlinked」(升级期间、已安装文件被 unlink 之前,优先使用解压出的包)所描述的机制,也是conda-launchers包在事务中尚未完成链接时,代码要优先从source_package_infos(正在解压的包信息)而不是前缀中查找的原因。

该行为自 conda 26.3.0 起引入(PR #15678);更早的版本在创建入口点时复制的是Scripts/conda.exe

cli-32.exe:win-32 目标的回退方案

cli-32.exe的角色相对单一:当已安装的或本次事务选定的conda-launchers包没有提供 32 位启动器时,作为回退。

这与上游渠道现状直接相关:releases/qa/16293-conda-launchers中说明,默认渠道(defaults)并未为win-32提供启动器,因此 conda 为win-32目标保留了自带的内置 stub。在源码层面,该回退逻辑位于 conda/core/launchers.py:

# Retain the bundled stub until defaults publishes a 32-bit launcher. if subdir == "win-32": path = join(CONDA_PACKAGE_ROOT, "shell", "cli-32.exe") if not isfile(path): raise FileNotFoundError(f"Missing bundled Windows launcher {path!r}.") return ( path, "0170dda609519c088b1e4619a1e1d15a01701a6c514bb55f99a85fcbbd541631", )

注意该分支附带一个硬编码的 SHA-256 摘要——即使走回退路径,文件仍会经过完整性校验(见下文「哈希校验」),损坏的cli-32.exe无法通过。

源码视角:stub 的定位、校验与回退全链路

要真正理解这两个文件的地位,需要看它们在整个入口点创建流程中的位置。核心模块是 conda/core/launchers.py,它提供两个函数:

  • get_windows_launcher_stub(target_prefix, *, source_prefixes, source_package_infos=()):定位目标 Python 对应的启动器路径及其期望的 SHA-256;
  • verify_windows_launcher(path, sha256):在复制前立即校验文件哈希,不匹配则抛出SafetyError

平台到 stub 的映射表

conda/base/constants.py 定义了WINDOWS_LAUNCHER_STUB_PATH

# Paths relative to the prefix containing the conda-launchers package. WINDOWS_LAUNCHER_STUB_PATH: Final = { "win-32": "share/conda-launchers/cli-32.exe", "win-64": "share/conda-launchers/cli-64.exe", "win-arm64": "share/conda-launchers/cli-arm64.exe", }

它给出了conda-launchers包内各平台 stub 的相对路径(相对安装前缀);内置的shell/cli-64.exeshell/cli-32.exe则是conda-launchers出现之前的同源文件。win-arm64也映射到cli-64.exe所在位置的 ARM64 变体cli-arm64.exe,即conda-launchers提供原生 ARM64 启动器(依赖 Windows 的 x64-on-ARM64 模拟兜底,见 CHANGELOG.md)。

选择优先级:事务包 > 前缀中已安装包 > 内置回退

get_windows_launcher_stub的执行顺序可以概括为:

  1. 确定目标 subdir:优先取事务中python包的 subdir(source_package_infos中名为pythonrepodata_record),否则取目标前缀中已安装的python记录,再否则回退到context.subdir。这样在交叉架构场景(如 win-32 主机创建 win-64 环境)也能选对 stub;
  2. 优先使用解压中的conda-launchers:如果本次事务包含conda-launchers,直接从其解压目录中查找short_path。这就是升级场景下的「先取新文件」逻辑;
  3. 其次在前缀中查找:遍历source_prefixes,通过PrefixData(prefix).get("conda-launchers")读取已安装记录,在paths_data中匹配short_path,并要求该路径数据带 SHA-256(缺失则抛SafetyError,对应测试 tests/core/test_launchers.py);
  4. win-32 专属回退:上述两步都失败且目标是win-32时,回退到内置CONDA_PACKAGE_ROOT/shell/cli-32.exe
  5. 否则报错:找不到时抛出FileNotFoundError,提示安装支持对应 subdir 的conda-launchers

上述行为均有对应单元测试覆盖,见 tests/core/test_launchers.py,例如:

  • test_launcher_matches_target_python:按目标 subdir 精确匹配(L64-L75);
  • test_launcher_matches_python_in_transaction:事务中 python 为 win-arm64 时也正确选择(L78-L101);
  • test_launcher_uses_extracted_package_during_upgrade:升级期间优先用解压出的新 launcher(L104 起);
  • test_launcher_win32_fallback*:win-32 回退内置 stub 及损坏文件被哈希校验拦截(L161-L217);
  • test_launcher_rejects_unsupported_target:linux-64 等非 Windows 目标抛出NotImplementedError(L245-L252)。

哈希校验与代码签名

verify_windows_launcher在复制前用compute_sum(path, "sha256")计算实际文件哈希并与期望值比对,任何不匹配都会触发SafetyError,这保证了即便选择了回退路径,文件也没有被篡改或损坏(对应test_launcher_win32_corrupt_fallback测试)。

此外,tests/test_codesigned.py 中的test_stub_exe_signatures使用 Windows SDK 的signtoolWINDOWS_LAUNCHER_STUB_PATH下的所有 stub(以及内置shell/cli-64.exe)执行签名验证(signtool verify /pa /v),确保发布的启动器带有合法代码签名,规避 SmartScreen 等安全提示。

两个消费方:conda init 与包安装事务

stub 选择逻辑被两处消费:

1. conda init:生成 Scripts/conda.exe

conda/core/initialize.py 的make_entry_point_execonda init时被调用:

def make_entry_point_exe(target_path, conda_prefix): from .launchers import get_windows_launcher_stub, verify_windows_launcher # target_path: join(conda_prefix, 'Scripts', 'conda.exe') exe_path = target_path source_exe_path, sha256 = get_windows_launcher_stub( conda_prefix, source_prefixes=(conda_prefix, context.conda_prefix) ) if isfile(exe_path): if compute_sum(exe_path, "sha256") == sha256: return Result.NO_CHANGE ... verify_windows_launcher(source_exe_path, sha256) copy(source_exe_path, exe_path)

它先比较目标文件现有哈希与期望哈希以判断是否需要更新(避免无意义写入),再校验源文件后复制。这也解释了 PR #16293 的变更:用 SHA-256 替代 MD5 来决定conda init是否替换已有启动器(见 releases/news/16293-conda-launchers-entrypoints)。

2. 包安装:为带入口点的包生成 Scripts/.exe

conda/core/path_actions.py 的create_python_entry_point_windows_exe_action在处理包含entry_points元数据的 Python 包时调用get_windows_launcher_stub,把选中的 stub 复制/链接为Scripts/<command>.exetarget_short_path = f"Scripts/{command}.exe"),随后同样执行哈希校验(L527-L529)。安装、更新、克隆环境等操作只要涉及 Windows 入口点都会经过这条路径。

这两个消费方分别覆盖「conda 自身入口点初始化」与「环境中任意 Python 包的入口点创建」,也正因如此,内置 stub 的生命周期牵动整个 Windows 用户群。

为什么不能直接删除:升级路径约束分析

conda/shell/README.md 的「Removal」一节给出了明确的移除条件:

  • 两个文件都没有排定的移除日期
  • 在移除之前,必须为「仍然会读取它们」的已发布客户端提供安全升级路径
  • 特别指出:用户可能跳过若干版本再升级,所以「再保留一个发布周期」并不能解决问题——只要还有老版本客户端在运行,新包就必须继续携带这些文件,否则老客户端发起自升级时会失败;
  • 移除cli-32.exe还额外要求:受支持的conda-launchers包必须能为win-32提供 32 位启动器(目前 defaults 渠道尚未提供)。

因此,这两个文件本质上承担的是向前兼容契约:它们是连接「旧 conda 进程」与「新 conda 包」之间升级事务的桥梁。相关质量保证说明见 releases/qa/16293-conda-launchers,其中也提到自动测试覆盖了「文件缺失、哈希缺失、启动器损坏、事务源选择」等场景。

演进脉络一览

从本仓库可以还原出这段历史的完整时间线:

  1. 26.3.0 之前:conda 创建 Windows 入口点时复制Scripts/conda.exe
  2. 26.3.0(PR #15678):改用内置 stubconda/shell/cli-32.exe/cli-64.exe创建入口点,使入口点创建不再依赖「conda 以 conda 包方式安装」(例如python -m pytest运行测试时也能工作),并通过平台映射表为win-arm64选择cli-64.exe,同时让conda init也使用目标平台对应的 stub,修复交叉架构安装(见 CHANGELOG.md);
  3. 后续(PR #16293):Windows 入口点改用发布的conda-launchers包(含原生 ARM64 启动器),复制前校验 SHA-256,conda init的替换判断改用 SHA-256 替代 MD5,同时保留内置 stub用于「已发布 conda 发起的升级」以及「无 32 位启动器的包」两类场景(见 releases/news/16293-conda-launchers-entrypoints)。

目前(含 26.7.2)仍处于第三阶段:conda-launchers是主路径,shell/cli-32.exeshell/cli-64.exe是兼容性保留路径。开发者如果需要在 Windows 上排查「入口点 .exe 创建失败」「conda init 报启动器相关错误」等问题,可以从 conda/core/launchers.py 的返回路径与 tests/core/test_launchers.py 的用例入手,快速定位是「conda-launchers 缺失」「哈希不匹配」还是「回退文件损坏」所致。

小结

conda/shell下的cli-32.execli-64.exe不是冗余资产,而是 conda 在 Windows 入口点机制迁移期精心设计的兼容层:cli-64.exe守护自升级事务的完成,cli-32.exe填补 defaults 渠道 32 位启动器的空缺;而它们的移除条件(安全升级路径 + 上游提供 32 位启动器)也清楚划定了 conda-launchers 生态成熟度的边界。理解这条链路,对 conda 的二次开发、打包以及 Windows 端故障排查都有直接价值。

【免费下载链接】condaA system-level, binary package and environment manager running on all major operating systems and platforms.项目地址: https://gitcode.com/GitHub_Trending/co/conda

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询