PEP 397 – 适用于 Windows 的 Python 启动器
- 作者:
- Mark Hammond <mhammond at skippinet.com.au>, Martin von Löwis <martin at v.loewis.de>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2011年3月15日
- Python 版本:
- 3.3
- 发布历史:
- 2011年7月21日,2011年5月17日,2011年3月15日
- 决议:
- Python-Dev 消息
摘要
本 PEP 描述了适用于 Windows 平台的 Python 启动器。Python 启动器是一个独立的执行文件,它使用多种启发式方法来定位 Python 可执行文件,并使用指定的命令行启动它。
基本原理
Windows 提供“文件关联”,因此可执行文件可以与扩展名关联,从而允许脚本在某些上下文中直接执行(例如,在 Windows 资源管理器中双击文件)。到目前为止,一直采用“最后安装的 Python 获胜”策略,虽然不理想,但由于 Python 2.x 版本中的保守更改,通常是可行的。由于 Python 3.x 脚本通常与 Python 2.x 脚本在语法上不兼容,因此必须使用不同的策略来允许“.py”扩展名的文件根据脚本目标 Python 版本使用不同的可执行文件。这将通过借鉴其他操作系统的现有做法来实现——脚本将能够通过“shebang”行指定它们所需的 Python 版本,如下所述。
类 Unix 操作系统(在本 PEP 中简称“Unix”)允许脚本像可执行镜像一样执行,方法是检查脚本中是否存在指定用于运行脚本的实际可执行文件的“shebang”行。这在 evecve(2) 手册页 [1] 中有详细描述,虽然将为此功能创建用户文档,但就本 PEP 而言,该手册页描述了一个有效的 shebang 行。
此外,这些操作系统在众所周知的目录中提供指向 Python 可执行文件的符号链接。例如,许多系统将有一个 /usr/bin/python 链接,它引用操作系统下安装的特定 Python 版本。这些符号链接允许 Python 在不考虑其在机器上实际安装位置的情况下执行(例如,无需在 shebang 行或 PATH 中引用 Python 实际安装的路径)。PEP 394 “Unix 类系统上的‘python’命令”描述了更精细指定特定 Python 版本的其他约定。
这两个设施结合起来,提供了一种可移植且在某种程度上可预测的方式来交互式启动 Python 和允许 Python 脚本执行。本 PEP 描述了一个启动器的实现,它可以在 Windows 平台上为 Python 提供相同的好处,因此允许启动器成为与“.py”文件关联的可执行文件,以并发支持多个 Python 版本。
虽然本 PEP 提供了使用 shebang 行的功能,该行应该在 Windows 和 Unix 上都有效,但这并不是本 PEP 的主要动机——主要动机是允许指定特定版本,而无需发明新的语法或约定来描述它。
规范
本 PEP 规定了启动器的功能;[3] 中提供了一个原型实现,它将随 Python 的 Windows 安装程序一起分发,但也将单独提供(但与 Python 安装程序一起发布)。只要此处规定的功能继续有效,就可以向启动器添加新功能。
安装
启动器有两种版本——一种是控制台程序,另一种是“Windows”(即 GUI)程序。这两个启动器对应于当前随 Python 提供的“python.exe”和“pythonw.exe”可执行文件。控制台启动器将命名为“py.exe”,Windows 启动器将命名为“pyw.exe”。“Windows”(即 GUI)版本的启动器将尝试定位并启动 pythonw.exe,即使虚拟 shebang 行仅指定“python”——事实上,虚拟 shebang 行根本不支持尾随的“w”表示法。
如果由特权用户安装,启动器将安装到 Windows 目录(参见下面的讨论)。独立安装程序会询问安装程序的替代位置,并将该位置添加到用户的 PATH 中。
Windows 目录中的安装是一个 32 位可执行文件(见讨论);独立安装程序也可能提供安装 64 位版本的启动器。
启动器安装在 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CurrentVersion\SharedDLLs 中注册,并带有一个引用计数器。它包含一个版本资源,与分发它的 pythonXY.dll 的版本号匹配。独立安装将用较新版本的启动器覆盖旧版本。独立发布在它们所基于的 CPython 版本的 FIELD3 中使用 0x10 的发布级别。
安装后,启动器的“控制台”版本与 .py 文件关联,“Windows”版本与 .pyw 文件关联。
启动器不与特定版本的 Python 绑定——例如,与 Python 3.3 一起分发的启动器应该能够定位并执行任何 Python 2.x 和 Python 3.x 版本。然而,启动器二进制文件具有的版本资源与它们发布的 Python 二进制文件中的版本资源相同。
Python 脚本启动
启动器仅限于启动 Python 脚本。它不打算用作通用的脚本启动器或 shebang 处理器。
启动器支持 [1] 中描述的 shebang 行语法,包括所有列出的限制。
启动器支持引用 Python 可执行文件的 shebang 行,这些行带有任何(正则表达式)前缀“/usr/bin/”、“/usr/local/bin”和“/usr/bin/env *”,以及未指定前缀的二进制文件。
例如,shebang 行“#! /usr/bin/python”应该有效,即使在相对的 Windows 目录“\usr\bin”中不太可能有可执行文件。这意味着许多脚本可以使用单个 shebang 行,并且可能在 Unix 和 Windows 上无需修改即可工作。
启动器将支持可执行文件的完全限定路径。虽然这会使脚本本质上不可移植,但它是 Unix 提供的一项功能,在某些情况下对 Windows 用户很有用。
启动器将能够支持 CPython 以外的实现,例如 Jython 和 IronPython,但鉴于 Unix 上没有通用链接(例如“/usr/bin/jython”),并且启动器无法在 Windows 上自动定位这些实现的安装位置,启动器将通过自定义选项来支持这一点。利用此功能的脚本将不可移植(因为必须设置这些自定义选项以反映运行启动器的机器的配置),但这一能力仍被认为是值得的。
在 Unix 上,用户可以通过调整 /usr/bin 中的链接指向所需版本来控制使用哪个特定版本的 Python。由于 Windows 上的启动器不使用 Windows 链接,因此将使用自定义选项(通过环境变量和 INI 文件公开)来覆盖确定将使用哪个 Python 版本的语义。例如,虽然“/usr/bin/python2”的 shebang 行将自动定位 Python 2.x 实现,但环境变量可以覆盖将选择哪个 Python 2.x 实现。对于“/usr/bin/python”和“/usr/bin/python3”也类似。这将在本 PEP 后面详细说明。
Shebang 行解析
如果第一个命令行参数不以短划线(‘-’)字符开头,则将尝试将该参数作为文件打开,并根据 [1] 中的规则解析 shebang 行。
#! interpreter [optional-arg]
解析后,命令将根据以下规则进行分类
- 如果命令以自定义命令的定义开头,后跟一个空白字符(包括换行符),则将使用自定义命令。有关自定义命令的描述,请参阅下文。
- 启动器将定义一组被视为 Unix 兼容命令的 Python 启动命令,即“/usr/bin/python”、“/usr/local/bin/python”、“/usr/bin/env python”和“python”。如果命令以其中一个字符串开头,则将被视为“虚拟命令”,并将使用 Python 版本限定符(下文)中描述的规则来定位要使用的可执行文件。
- 否则,假定该命令可以直接执行——即,一个完全限定的路径(或对
PATH上可执行文件的引用),可选地后跟参数。字符串内容不会被解析——它将直接传递给 Windows CreateProcess 函数,后面附加脚本名称和启动器命令行参数。这意味着将使用 CreateProcess 使用的规则,包括如何处理相对路径名和不带扩展名的可执行文件引用。值得注意的是,不会使用 Windows 命令处理器,因此不会使用命令处理器使用的特殊规则(例如自动附加除“.exe”之外的扩展名,支持批处理文件等)。
鼓励使用“虚拟”shebang 行,因为这应该允许指定可在多个操作系统和同一操作系统的不同安装上工作的可移植 shebang 行。
如果第一个参数无法作为文件打开,或者找不到有效的 shebang 行,启动器将像找到“#!python”的 shebang 行一样行事——即,将定位默认的 Python 解释器并将参数传递给它。但是,如果找到有效的 shebang 行但该行指定的进程无法启动,则不会启动默认解释器——创建指定子进程的错误将导致启动器显示适当的消息并以特定的退出代码终止。
配置文件
启动器将搜索两个 .ini 文件——当前用户“应用程序数据”目录中的 py.ini(即调用 Windows 函数 SHGetFolderPath 并使用 CSIDL_LOCAL_APPDATA 返回的目录,在 Vista+ 上为 %USERPROFILE%\AppData\Local,在 XP 上为 %USERPROFILE%\Local Settings\Application Data)以及启动器相同目录中的 py.ini。对于启动器的“控制台”版本(即 py.exe)和“Windows”版本(即 pyw.exe),都使用相同的 .ini 文件。
“应用程序目录”中指定的自定义优先于可执行文件旁边的自定义,因此可能对启动器旁边的 .ini 文件没有写入权限的用户可以覆盖该全局 .ini 文件中的命令。
Shebang 行中的虚拟命令
虚拟命令是 shebang 行,它们以在 Unix 平台上预期能工作的字符串开头——例如“/usr/bin/python”、“/usr/bin/env python”和“python”。可选地,虚拟命令可以带有版本限定符后缀(见下文),例如“/usr/bin/python2”或“/usr/bin/python3.2”。执行的命令基于下面 Python 版本限定符中描述的规则。
自定义命令
启动器将支持在 Windows .ini 文件中定义“自定义命令”的功能(即,可以通过 Windows 函数 GetPrivateProfileString 解析的文件)。可以创建一个名为“[commands]”的部分,其中键名定义虚拟命令,值指定用于此虚拟命令的实际命令行。
例如,如果 INI 文件包含以下内容
[commands]
vpython=c:\bin\vpython.exe -foo
那么在名为“doit.py”的脚本中的 shebang 行“#! vpython”将导致启动器使用命令行 c:\bin\vpython.exe -foo doit.py。
.ini 文件的名称、位置和搜索顺序的精确细节在启动器文档 [4] 中。
Python 版本限定符
描述的一些功能允许使用可选的 Python 版本限定符。
版本限定符以主版本号开头,后面可以选择跟一个句点(‘.’)和一个次版本说明符。如果指定了次版本限定符,则可以选择后跟“-32”以指示使用该版本的 32 位实现。请注意,不需要“-64”限定符,因为这是默认实现(见下文)。
在同时安装了相同(主版本.次版本)Python 版本的 32 位和 64 位实现的 64 位 Windows 上,将始终优先选择 64 位版本。这对于启动器的 32 位和 64 位实现都成立——如果可用,32 位启动器将优先执行指定版本的 64 位 Python 安装。这是为了使启动器的行为可以预测,只需了解 PC 上安装了哪些版本,而无需考虑它们的安装顺序(即,无需知道是 32 位还是 64 位版本的 Python 和相应的启动器最后安装)。如上所述,可以在版本说明符上使用可选的“-32”后缀来更改此行为。
如果在命令中未找到版本限定符,则可以设置环境变量 PY_PYTHON 来指定默认版本限定符——默认值为“2”。请注意,此值可以仅指定主版本(例如“2”)或主版本.次版本限定符(例如“2.6”),甚至主版本.次版本-32。
如果未找到次版本限定符,则可以设置环境变量 PY_PYTHON{major}(其中 {major} 是如上所述确定的当前主版本限定符)以指定完整版本。如果未找到此类选项,启动器将枚举已安装的 Python 版本,并使用为主版本找到的最新次要版本,这很可能(但不保证)是该系列中最近安装的版本。
除了环境变量之外,相同的设置也可以在启动器使用的 .INI 文件中配置。.INI 文件中的部分名为 [defaults],键名将与不带前导 PY_ 前缀的环境变量相同(请注意 .INI 文件中的键名不区分大小写)。环境变量的内容将覆盖 .INI 文件中指定的内容。
命令行处理
只有第一个命令行参数会被检查 shebang 行,并且只有当该参数不以‘-’开头时。
如果唯一的命令行参数是“-h”或“–help”,启动器将打印一个小的横幅和命令行用法,然后将参数传递给默认的 Python。这将导致打印启动器的帮助,然后打印 Python 本身的帮助。启动器的输出将清楚地表明扩展帮助信息来自启动器而不是 Python。
作为交互式启动 Python 的一种便利,启动器将支持第一个命令行参数可选地为短划线(“-”),后跟版本限定符,如上所述,以指定要使用的特定版本。例如,虽然“py.exe”可以定位并启动最新安装的 Python 2.x 实现,但诸如“py.exe -3”的命令行可以指定启动最新的 Python 3.x 实现,而“py.exe -2.6-32”可以指定定位并启动 32 位实现的 Python 2.6。如果希望使用 -3 标志启动 Python 2.x 实现,命令行将需要类似于“py.exe -2 -3”(或者显然可以手动启动特定版本的 Python 而不使用此启动器)。请注意,此功能不能与 shebang 处理一起使用,因为扫描 shebang 行的文件和此参数都必须是第一个参数,因此它们是互斥的。
所有其他参数将原封不动地传递给子 Python 进程。
进程启动
启动器为交互式工作的 Python 开发人员提供了一些便利——例如,不带命令行参数启动启动器将启动不带命令行参数的默认 Python。此外,将支持命令行参数以允许交互式启动特定 Python 版本——但是,这些便利不得偏离启动脚本的主要目的,并且如果需要,必须易于避免。
启动器创建一个子进程来启动实际的解释器。有关理由,请参阅下面的“讨论”。
讨论
启动器安装到 Windows 目录而不是 System32 目录可能令人惊讶。原因是 System32 目录不在 64 位系统上运行的 32 位进程的 Path 中。但是,Windows 目录始终在 Path 中。
安装到 Windows 目录的启动器是一个 32 位可执行文件,以便 32 位 CPython 安装程序可以为 32 位和 64 位 Windows 安装提供相同的二进制文件。
理想情况下,启动器进程会在同一个进程内直接执行 Python,主要是为了让启动器进程的父进程可以终止启动器并让 Python 解释器终止。如果启动器将 Python 作为子进程执行,并且启动器进程的父进程终止启动器,则 Python 进程将不受影响。
然而,这种方法存在许多实际问题。Windows 不支持 Unix 函数家族中的 execv*,因此这只能通过启动器动态加载 Python DLL 来完成,但这会带来许多副作用。最严重的副作用是 sys.executable 的值将引用启动器而不是 Python 实现。许多 Python 脚本使用 sys.executable 的值来启动子进程,如果使用启动器,这些脚本可能无法按预期工作。考虑一个 shebang 行是“#! /usr/bin/python3”的“父”脚本,它尝试通过 sys.executable 启动一个子脚本(没有 shebang)——目前子脚本使用运行父脚本的完全相同的版本启动。如果 sys.executable 引用启动器,子脚本很可能使用 Python 2.x 版本执行,并且很可能因 SyntaxError 而失败。
另一个障碍是支持使用上面描述的“自定义命令”功能来实现替代 Python 实现,在这种情况下,将命令动态加载到正在运行的可执行文件中是不可能的。
最后一个障碍是上面关于 64 位和 32 位程序的规则——一个 32 位启动器将无法加载 64 位版本的 Python,反之亦然。
考虑到这些因素,启动器将在子进程中执行其命令,在子进程执行期间保持活动状态,然后以子进程返回的相同退出代码终止。为了解决启动器终止不杀死子进程的问题,将使用 Win32 Job API 来安排在父进程终止时自动杀死子进程(尽管该子进程的子进程将继续,就像现在一样)。由于此 Windows API 在 Windows XP 及更高版本中可用,因此此启动器将无法在 Windows 2000 或更早版本上工作。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0397.rst
最后修改: 2025-02-01 08:55:40 GMT