将预编译的扩展放在非纯Python Wheel包的根文件夹中

将预编译的扩展放在非纯Python Wheel包的根文件夹中,python,setuptools,distutils,Python,Setuptools,Distutils,TL;DR如何让distutils/setuptools正确包含非纯数据文件 我有一个项目,它使用一个定制的工具链生成一些非平凡的代码,然后用SWIG包装生成的代码,最后构建Python扩展。Cmake出色地封装了所有这些,最后我在项目的根目录中有一个文件夹,它与任何其他Python包一样工作 我想要一个简单的setup.py,这样我就可以将这个包打包成一个轮子,然后将它发送给PyPI,这样普通Python用户就不必处理构建过程。关于如何强制setuptools生成一个非纯轮子,然后您可以使用p

TL;DR如何让distutils/setuptools正确包含非纯数据文件

我有一个项目,它使用一个定制的工具链生成一些非平凡的代码,然后用SWIG包装生成的代码,最后构建Python扩展。Cmake出色地封装了所有这些,最后我在项目的根目录中有一个文件夹,它与任何其他Python包一样工作

我想要一个简单的
setup.py
,这样我就可以将这个包打包成一个轮子,然后将它发送给PyPI,这样普通Python用户就不必处理构建过程。关于如何强制setuptools生成一个非纯轮子,然后您可以使用
package\u data
字段或
MANIFEST.in
文件捆绑扩展,有很多答案

问题是,这种方法会导致控制盘格式不正确,因为扩展被包含在
purelib
下,而不是根目录下(它们属于
根目录的位置是Pure:False
控制盘)。一些工具和发行版依赖于这种正确的分离


我不感兴趣的答案:从
setup.py
中运行cmake的自定义扩展(不想为配置项目添加另一个间接层,不想在构建选项更改时维护它),修改生成的控制盘,我更愿意避免向项目根目录添加更多的文件,而不仅仅是
setup.py

这样做有效
distutils
setuptools
必须是现有中央Python基础设施中设计最差的部分

from setuptools import setup, find_packages, Extension
from setuptools.command.build_ext import build_ext
import os
import pathlib
import shutil

suffix = '.pyd' if os.name == 'nt' else '.so'

class CustomDistribution(Distribution):
  def iter_distribution_names(self):
    for pkg in self.packages or ():
      yield pkg
    for module in self.py_modules or ():
      yield module

class CustomExtension(Extension):
  def __init__(self, path):
    self.path = path
    super().__init__(pathlib.PurePath(path).name, [])

class build_CustomExtensions(build_ext):
  def run(self):
    for ext in (x for x in self.extensions if isinstance(x, CustomExtension)):
      source = f"{ext.path}{suffix}"
      build_dir = pathlib.PurePath(self.get_ext_fullpath(ext.name)).parent
      os.makedirs(f"{build_dir}/{pathlib.PurePath(ext.path).parent}",
          exist_ok = True)
      shutil.copy(f"{source}", f"{build_dir}/{source}")

def find_extensions(directory):
  extensions = []
  for path, _, filenames in os.walk(directory):
    for filename in filenames:
      filename = pathlib.PurePath(filename)
      if pathlib.PurePath(filename).suffix == suffix:
        extensions.append(CustomExtension(os.path.join(path, filename.stem)))
  return extensions

setup(
  # Stuff
  ext_modules = find_extensions("PackageRoot"),
  cmdclass = {'build_ext': build_CustomExtensions}
  distclass = CustomDistribution
)

我正在将扩展复制到构建目录,就这样。我们覆盖了发行版,向egg信息编写者谎称有任何扩展,一切都很重要。

Python扩展应该在安装脚本中构建,使用通过
ext_modules
@hoefling提供的源代码和配置,不幸的是,这需要维护一个自定义setup.py构建扩展来运行生成代码的脚本,复制作业cmake和Ninja已经做得更好了。我在问题中明确提到了这一点,但这并不是第一个问题。因此,IIUC您想告诉安装脚本,您正在构建一个带有扩展的控制盘,而不提供扩展,并将预构建的共享对象打包为包数据吗?这还需要维护自定义
setup.py
hacks,这比扩展
build\u ext
糟糕得多。如果CMake更适合您,我建议使用一个自定义目标扩展构建指令,该目标将组装控制盘、编写控制盘元数据并压缩内容。@hoefling当然可以,但这方面的技巧很简单,发行版的子类是三行python()。如果你有一个类似的小问题,那么这就是这个问题的全部内容。现在我有一个用大约20行Python编写的ish解决方案,它比尝试用cmake组装轮子或教setup.py关于cmake要简单得多。
top\u level.txt
是在执行
easy\u install
命令时生成的(IIRC它被
install
调用)。相关代码段:@hoefling,这是在Windows上使用easy_install命令将exe作为egg安装时写入安装目录的top_level.txt(而且easy_install已被弃用)。它和构建轮子时由distutils生成的函数无关,相信我,我尝试过修改那个确切的函数只是为了确保。我同意这是distutils、setuptools和wheel build extension中唯一提到“顶级”的地方。现在你知道我的沮丧了。你的评论促使我找到了作者,我找到了。它是从iter分布名称中提取的,也许我可以修改它以获得所需的行为ep,my bad-看起来像是函数。它是通过注册的,因此在源代码中没有直接提到它。总的来说,这就是我的观点——覆盖distutils以实现定制行为的任务通常非常繁琐,最终不值得这么做。编辑:您首先得到了writer,虽然:-)是的,但是现在我们得到了它,只需要覆盖
iter\u distribution\u names
方法以不返回扩展名,我们就完成了。25行代码,实现了所需的行为,比其他任何东西都要简短和简单。如果distutils和setuptools没有过度设计的话,那么弄清楚那25行是什么就很简单了。