面向配方作者的 Python

本文档解释如何在 Homebrew 配方中成功使用 Python。

Homebrew 区分 Python 应用程序和 Python 。区别在于,用户通常不关心应用程序是用 Python 编写的;用户期望在安装应用程序后能够 import foo 的情况并不常见。应用程序的示例包括 ansiblejrnl

Python 库的存在是为了供其他 Python 模块导入;它们通常是 Python 应用程序的依赖项。它们通常在终端中用处不大。库的示例包括 certifinumpy

绑定是库的一种特殊情况,它允许 Python 代码与用另一种语言实现的库或应用程序进行交互。一个示例是 libxml2 安装的 Python 绑定。

Homebrew 乐于接受用 Python 构建的应用程序,无论这些应用程序是否可从 PyPI 获得。Homebrew 通常不会接受可以用 pip install foo 正确安装的库。可以为提供它们的软件包安装绑定,特别是如果 pip 无法提供同等功能时。同样,具有大量本机代码且因此编译时间长的库也可能是不错的候选者。但如果有疑问:不要打包库。

应用程序应无条件地捆绑所有其 Python 语言依赖项和库,并应安装任何未满足的依赖项;以下部分将深入讨论这些策略。

应用程序

应用程序的 Python 声明

需要 Python 3 的应用程序的配方必须声明对 "[email protected]" 的无条件依赖。这些应用程序必须与当前的 Homebrew Python 3.y 配方一起使用。

安装应用程序

[email protected] 开始,Homebrew 遵循 PEP 668。应用程序必须安装到根目录为 libexec 的 Python 虚拟环境 中。这可防止应用程序的 Python 模块污染系统 site-packages,反之亦然。

应用程序的所有 Python 模块依赖项(及其依赖项,递归)都应在公式中声明为 resource,并安装到虚拟环境中。每个依赖项都应明确指定;请不要依赖 setup.pypip 执行自动依赖项解析,原因 在此处进行了说明。

你可以使用 brew update-python-resources 来帮助你编写资源节。要使用它,只需运行 brew update-python-resources <formula>。有时,brew update-python-resources 将无法自动更新资源。如果发生这种情况,请尝试运行 brew update-python-resources --print-only <formula> 以打印资源节,而不是将更改直接应用到文件中。然后,你可以根据需要复制和粘贴资源。

如果使用 brew update-python-resources 不起作用,你可以使用 homebrew-pypi-poet 来帮助你编写资源节。要使用它,请设置一个虚拟环境并安装你的软件包及其所有依赖项。然后,在同一个虚拟环境中 pip install homebrew-pypi-poet。运行 poet some_package 将生成必要的资源节。你可以像这样操作

# Use a temporary directory for the virtual environment
cd "$(mktemp -d)"

# Create and source a new virtual environment in the venv/ directory
python3 -m venv venv
source venv/bin/activate

# Install the package of interest as well as homebrew-pypi-poet
pip install some_package homebrew-pypi-poet
poet some_package

# Destroy the virtual environment
deactivate
rm -rf venv

Homebrew 提供了用于实例化和填充虚拟环境的帮助程序方法。你可以通过在 Formula 类定义的顶部放置 include Language::Python::Virtualenv 来使用它们。

对于大多数应用程序,你只需要编写

class Foo < Formula
  include Language::Python::Virtualenv

  # ...
  url "https://example.com/foo-1.0.tar.gz"
  sha256 "abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1"

  depends_on "[email protected]"

  def install
    virtualenv_install_with_resources
  end
end

这与编写完全相同

class Foo < Formula
  include Language::Python::Virtualenv

  # ...
  url "https://example.com/foo-1.0.tar.gz"
  sha256 "abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1"

  depends_on "[email protected]"

  def install
    # Create a virtualenv in `libexec`.
    venv = virtualenv_create(libexec, "python3.y")
    # Install all of the resources declared on the formula into the virtualenv.
    venv.pip_install resources
    # `pip_install_and_link` takes a look at the virtualenv's bin directory
    # before and after installing its argument. New scripts will be symlinked
    # into `bin`. `pip_install_and_link buildpath` will install the package
    # that the formula points to, because buildpath is the location where the
    # formula's tarball was unpacked.
    venv.pip_install_and_link buildpath
  end
end

示例公式

安装具有依赖项的公式将如下所示

class Foo < Formula
  include Language::Python::Virtualenv

  desc "Description"
  homepage "https://example.com"
  url "..."

  resource "six" do
    url "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz"
    sha256 "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"
  end

  resource "parsedatetime" do
    url "https://files.pythonhosted.org/packages/a8/20/cb587f6672dbe585d101f590c3871d16e7aec5a576a1694997a3777312ac/parsedatetime-2.6.tar.gz"
    sha256 "4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455"
  end

  def install
    virtualenv_install_with_resources
  end
end

您还可以使用更详细的形式,并请求安装特定资源

class Foo < Formula
  include Language::Python::Virtualenv

  desc "Description"
  homepage "https://example.com"
  url "..."

  def install
    venv = virtualenv_create(libexec)
    %w[six parsedatetime].each do |r|
      venv.pip_install resource(r)
    end
    venv.pip_install_and_link buildpath
  end
end

以防您需要对不同的资源执行不同的操作。

绑定

要为 Python 3 添加绑定,请添加 depends_on "[email protected]" 以配合当前 Homebrew Python 3.y 公式。

绑定依赖项

绑定应遵循与库相同的 Python 模块依赖项建议;请参阅以下内容以了解更多信息。

安装绑定

如果通过调用 setup.py 安装绑定,请执行类似以下操作

system "python3.y", "-m", "pip", "install", *std_pip_args(build_isolation: true), "./source/python"

Autotools

如果配置脚本采用 --with-python 标志,它通常不需要额外的帮助来查找 Python。但是,如果依赖项树中有多个 Python 公式,则可能需要帮助来查找正确的公式。

如果 configuremake 脚本不想安装到 Cellar,有时您可以

  1. 调用 ./configure --without-python(或类似的命名选项)
  2. 对包含 Python 绑定的目录调用 pip(如上所述)

有时,我们必须使用 Homebrew 的 inreplace 帮助器方法即时编辑 Makefile,以使用我们的前缀来进行 Python 绑定。

CMake

如果 cmake 找到的 Python 与直接依赖项不同,有时您可以通过使用 -D 选项设置以下变量之一来帮助它找到正确的 Python

Meson

作为 Homebrew 符号链接安装和 Python sysconfig 补丁的副作用,meson 可能无法自动检测到将 Python 绑定安装到的 Cellar 目录。如果公式的 meson 构建定义使用 install_sources() 或类似方法,您可以设置 python.purelibdir 和/或 python.platlibdir 以覆盖默认路径。

如果 meson 找到的 Python 与直接依赖项不同,并且公式的 meson 选项定义文件未提供用户可设置的选项,那么您需要检查如何检测 Python 可执行文件。一种常见的方法是 find_installation() 方法,该方法将根据 name_or_path 参数的设置而表现不同。

记住:库的使用场景非常有限(例如,编译了大量原生代码),因此,如有疑问,请不要打包它们。

我们不用 python- 前缀来表示这类公式!

homebrew-core 中允许的库示例

库的 Python 声明

为 Python 3 构建的库必须包含 depends_on "[email protected]",它将针对 Homebrew 的 Python 3.y 进行装瓶。

安装库

可以通过编写 .pth 文件(命名为“homebrew-foo.pth”)到 prefix site-packages,将库安装到 libexec 并将其添加到 sys.path。如果意外使用 pip 升级 Homebrew 安装的软件包,这将简化随之而来的问题,并防止在 Homebrew 的 site-packages 中累积过期的 .pyc 文件。

目前,大多数公式都只安装到 prefix。任何过期的 .pyc 文件都由 brew cleanup 处理。

库的依赖项

必须安装库依赖项,以便可以导入它们。为了最大程度地减少链接冲突的可能性,应将依赖项安装到 libexec/<vendor> 并通过编写第二个 .pth 文件(命名为“homebrew-foo-dependencies.pth”)到 prefix site-packages 将其添加到 sys.path

具有通用 Python 库依赖项(例如 setuptoolssix)的公式不应使用此方法,因为它会使用安装在 libexec/<vendor> 中的所有库污染系统 site-packages

深入探讨

额外的评论,解释了 Homebrew 为什么执行某些操作。

Setuptools 与 Distutils 与 pip

Distutils 是 Python 标准库中的一个模块,在 Python 3.12 中删除之前,它为开发者提供了一个基本的包管理 API。Setuptools 是一个在标准库外部分发的模块,它扩展并取代了 Distutils。根据惯例,Python 包会提供一个 setup.py,它调用 Distutils 或 Setuptools 中的 setup() 函数。

Setuptools 过去提供 easy_install 命令,这是一个终端用户包管理工具,用于从 PyPI(Python 包索引)获取并安装包。easy_install 控制台脚本已在 Setuptools v52.0.0 中删除,自 v58.3.0 起已弃用直接使用。pip 是另一个较新的终端用户包管理工具,它也在标准库外部提供。虽然 pip 取代了 easy_install,但它并没有取代 Setuptools 模块的其他功能。

Distutils 和 pip 使用“扁平”安装层次结构,将模块作为单个文件安装在 site-packages 下,而 easy_install 则将压缩的 egg 安装到 site-packages 中。

Distribute(不要与 Distutils 混淆)是 Setuptools 的一个过时的分支。Distlib 是标准库外部维护的一个包,pip 将其用于一些低级打包操作,与大多数 setup.py 用户无关。

运行 setup.py

当公式需要与 setup.py 交互而不是调用 pip 时,Homebrew 提供了帮助程序方法 Language::Python.setup_install_args,它返回用于调用 setup.py 的有用参数。你的公式应使用此方法,而不是显式调用 setup.py。语法为

system Formula["[email protected]"].opt_bin/"python3.y", *Language::Python.setup_install_args(prefix)

其中 prefix 是目标前缀(通常为 libexecprefix)。

什么是 --single-version-externally-managed

--single-version-externally-managed(“SVEM”)是 Setuptools 专用于 setup.py install 的参数。SVEM 的主要作用是使用 Distutils 执行安装,而不是 Setuptools 的 easy_install

easy_install 做了一些我们需要避免的事情

Setuptools 要求 SVEM 与 --record 结合使用,后者提供一个文件列表,可供之后卸载软件包时使用。我们不需要或不想要这个,因为 Homebrew 可以管理卸载,但由于 Setuptools 要求,我们遵守。Homebrew 惯例是将记录文件命名为“installed.txt”。

检测 setup.py 是否使用 Setuptools 或 Distutils 的 setup() 很困难,但我们始终需要将此标志传递给基于 Setuptools 的脚本。 pip 面临与我们相同的问题,并强制 setup() 使用 Setuptools 版本,方法是在 setup.py 周围加载一个垫片,在执行任何其他操作之前导入 Setuptools。由于 Setuptools 对 Distutils 进行猴子补丁并替换其 setup 函数,因此这提供了一个单一的、一致的界面。我们借用了此代码,并在 Language::Python.setup_install_args 中使用它。

--prefix--root

setup.py 接受一系列令人困惑的安装选项。Homebrew 的正确开关是 --prefix,它会自动使用合理的 POSIX-y 值设置 --install-foo 系列选项。

当安装到不会成为文件最终安装位置的前缀中时(例如,在构建 RPM 或二进制发行版时),使用 --root。在使用基于 Setuptools 的 setup.py 时,--root 具有激活 --single-version-externally-managed 的副作用。将 --root 与空 --prefix 一起使用是不安全的,因为在字节编译模块时,root 会从路径中删除。

--prefix--root=/ 一起使用可能是安全的,它应该适用于基于 Setuptools 或 Distutils 的 setup.py,但这有点丑陋。

pipsetup.py

PEP 453 向下游分销商(我们)提出建议,即应使用 pip 安装 sdist tarball,而不是直接调用 setup.py。出于历史原因,我们没有遵循 PEP 453,因此一些公式仍然使用 setup.py 安装。如今,大多数核心公式都使用 pip,因为我们已将它们迁移到这种首选安装方法。

Fork me on GitHub