【问题标题】:Package only binary compiled .so files of a python library compiled with Cython仅打包使用 Cython 编译的 python 库的二进制编译的 .so 文件
【发布时间】:2017-01-22 19:00:18
【问题描述】:

我有一个名为mypack 的包,里面有一个模块mymod.py,并且 __init__.py。 出于某种没有争议的原因,我需要打包这个模块编译 (也不允许 .py 或 .pyc 文件)。也就是说,__init__.py 是唯一的 分布式压缩文件中允许的源文件。

文件夹结构为:

. 
│  
├── mypack
│   ├── __init__.py
│   └── mymod.py
├── setup.py

我发现 Cython 能够通过转换 .so 库中的每个 .py 文件来做到这一点 可以直接用python导入。

问题是:setup.py 文件必须如何才能轻松打包和安装?

目标系统有一个 virtualenv 必须安装包 任何允许轻松安装和卸载的方法(easy_install、pip 等都是 欢迎)。

我尽我所能。我阅读了setuptoolsdistutils 文档, 所有与stackoverflow相关的问题, 并尝试了各种命令(sdist、bdist、bdist_egg 等),有很多 setup.cfg 和 MANIFEST.in 文件条目的组合。

我得到的最接近的是下面的设置文件,它将子类化 bdist_egg 命令以删除 .pyc 文件,但这会破坏安装。

在 venv 中“手动”安装文件的解决方案是 也很好,前提是所有辅助文件都包含在适当的 安装已涵盖(我需要在 venv 中运行 pip freeze 并查看 mymod==0.0.1)。

运行它:

python setup.py bdist_egg --exclude-source-files

并(尝试)使用

安装它
easy_install mymod-0.0.1-py2.7-linux-x86_64.egg

您可能会注意到,目标是带有 python 2.7 的 linux 64 位。

from Cython.Distutils import build_ext
from setuptools import setup, find_packages
from setuptools.extension import Extension
from setuptools.command import bdist_egg
from setuptools.command.bdist_egg import  walk_egg, log 
import os

class my_bdist_egg(bdist_egg.bdist_egg):

    def zap_pyfiles(self):
        log.info("Removing .py files from temporary directory")
        for base, dirs, files in walk_egg(self.bdist_dir):
            for name in files:
                if not name.endswith('__init__.py'):
                    if name.endswith('.py') or name.endswith('.pyc'):
                        # original 'if' only has name.endswith('.py')
                        path = os.path.join(base, name)
                        log.info("Deleting %s",path)
                        os.unlink(path)

ext_modules=[
    Extension("mypack.mymod", ["mypack/mymod.py"]),
]

setup(
  name = 'mypack',
  cmdclass = {'build_ext': build_ext, 
              'bdist_egg': my_bdist_egg },
  ext_modules = ext_modules,
  version='0.0.1',
  description='This is mypack compiled lib',
  author='Myself',
  packages=['mypack'],
)

更新。 在@Teyras 回答之后,可以按照回答中的要求建造一个轮子。 setup.py 文件内容为:

import os
import shutil
from setuptools.extension import Extension
from setuptools import setup
from Cython.Build import cythonize
from Cython.Distutils import build_ext

class MyBuildExt(build_ext):
    def run(self):
        build_ext.run(self)
        build_dir = os.path.realpath(self.build_lib)
        root_dir = os.path.dirname(os.path.realpath(__file__))
        target_dir = build_dir if not self.inplace else root_dir
        self.copy_file('mypack/__init__.py', root_dir, target_dir)

    def copy_file(self, path, source_dir, destination_dir):
        if os.path.exists(os.path.join(source_dir, path)):
            shutil.copyfile(os.path.join(source_dir, path), 
                            os.path.join(destination_dir, path))


setup(
  name = 'mypack',
  cmdclass = {'build_ext': MyBuildExt},
  ext_modules = cythonize([Extension("mypack.*", ["mypack/*.py"])]),
  version='0.0.1',
  description='This is mypack compiled lib',
  author='Myself',
  packages=[],
  include_package_data=True )

关键是设置packages=[],。需要覆盖build_extrun 方法才能将__init__.py 文件放入轮子中。

【问题讨论】:

    标签: python cython setuptools distutils setup.py


    【解决方案1】:

    不幸的是,the answer suggesting setting packages=[] 是错误的,可能会破坏很多东西,例如见this question。不要使用它。而不是从 dist 中排除所有包,您应该只排除将被 cythonized 并编译为共享对象的 python 文件。

    以下是一个工作示例;它使用来自问题Exclude single source file from python bdist_egg or bdist_wheelmy recipe。示例项目包含包spam 和两个模块spam.eggsspam.bacon,以及一个子包spam.fizz 和一个模块spam.fizz.buzz

    root
    ├── setup.py
    └── spam
        ├── __init__.py
        ├── bacon.py
        ├── eggs.py
        └── fizz
            ├── __init__.py
            └── buzz.py
    

    模块查找是在build_py 命令中完成的,因此您需要使用自定义行为进行子类化。

    简单案例:编译所有源代码,不例外

    如果您要编译每个.py 文件(包括__init__.pys),覆盖build_py.build_packages 方法已经足够了,使其成为noop。因为build_packages 不做任何事情,所以根本不会收集.py 文件,并且 dist 将只包含 cythonized 扩展:

    import fnmatch
    from setuptools import find_packages, setup, Extension
    from setuptools.command.build_py import build_py as build_py_orig
    from Cython.Build import cythonize
    
    
    extensions = [
        # example of extensions with regex
        Extension('spam.*', ['spam/*.py']),
        # example of extension with single source file
        Extension('spam.fizz.buzz', ['spam/fizz/buzz.py']),
    ]
    
    
    class build_py(build_py_orig):
        def build_packages(self):
            pass
    
    
    setup(
        name='...',
        version='...',
        packages=find_packages(),
        ext_modules=cythonize(extensions),
        cmdclass={'build_py': build_py},
    )
    

    复杂案例:将 cythonized 扩展与源模块混合

    如果你想只编译选定的模块而保持其余部分不变,你将需要更复杂的逻辑;在这种情况下,您需要覆盖模块查找。在下面的示例中,我仍然将spam.baconspam.eggsspam.fizz.buzz 编译为共享对象,但保持__init__.py 文件不变,因此它们将被包含为源模块:

    import fnmatch
    from setuptools import find_packages, setup, Extension
    from setuptools.command.build_py import build_py as build_py_orig
    from Cython.Build import cythonize
    
    
    extensions = [
        Extension('spam.*', ['spam/*.py']),
        Extension('spam.fizz.buzz', ['spam/fizz/buzz.py']),
    ]
    cython_excludes = ['**/__init__.py']
    
    
    def not_cythonized(tup):
        (package, module, filepath) = tup
        return any(
            fnmatch.fnmatchcase(filepath, pat=pattern) for pattern in cython_excludes
        ) or not any(
            fnmatch.fnmatchcase(filepath, pat=pattern)
            for ext in extensions
            for pattern in ext.sources
        )
    
    
    class build_py(build_py_orig):
        def find_modules(self):
            modules = super().find_modules()
            return list(filter(not_cythonized, modules))
    
        def find_package_modules(self, package, package_dir):
            modules = super().find_package_modules(package, package_dir)
            return list(filter(not_cythonized, modules))
    
    
    setup(
        name='...',
        version='...',
        packages=find_packages(),
        ext_modules=cythonize(extensions, exclude=cython_excludes),
        cmdclass={'build_py': build_py},
    )
    

    【讨论】:

    • 我将此标记为正确答案,因为您似乎很清楚自己在说什么。不过,我会尝试该代码,并让您知道它是否对我有用。感谢您的贡献!
    • 很高兴我能帮上忙!如果应该使用您的特定用例的示例代码更新答案,请告诉我。
    • 我无法让它在 Python 3.7.0 上运行。一方面, build_py 似乎返回了一个过滤器对象,因此 filter() 必须包含在 list() 调用中。其次,即使过滤结果对我来说是正确的,它对包装完全没有影响。 py 文件仍在包含中。
    • @hoefling 我可以确认更新版本适用于 3.7。
    • @hoefling:我已经尝试过您的方法,但似乎原始 c 文件包含在轮子中。这是预期的吗?
    【解决方案2】:

    虽然打包为轮子绝对是您想要的,但最初的问题是关于从包中排除 .py 源文件。 @Teyras 在 Using Cython to protect a Python codebase 中解决了这个问题,但他的解决方案使用了一个技巧:它从对 setup() 的调用中删除了 packages 参数。这会阻止 build_py 步骤运行,该步骤确实排除了 .py 文件,但它也排除了您希望包含在包中的任何数据文件。 (例如,我的包有一个名为 VERSION 的数据文件,其中包含包版本号。)更好的解决方案是将 build_py 设置命令替换为仅复制数据文件的自定义命令。

    您还需要如上所述的__init__.py 文件。所以自定义 build_py 命令应该创建__init_.py 文件。我发现编译的__init__.so 在导入包时运行,所以只需要一个空的__init__.py 文件来告诉Python 该目录是一个可以导入的模块。

    您的自定义 build_py 类如下所示:

    import os
    from setuptools.command.build_py import build_py
    
    class CustomBuildPyCommand(build_py):
        def run(self):
            # package data files but not .py files
            build_py.build_package_data(self)
            # create empty __init__.py in target dirs
            for pdir in self.packages:
                open(os.path.join(self.build_lib, pdir, '__init__.py'), 'a').close()
    

    并配置setup来覆盖原来的build_py命令:

    setup(
       ...
       cmdclass={'build_py': CustomBuildPyCommand},
    )
    

    【讨论】:

    • 伟大的观察。不过我标记了@Teyras 答案,因为他的答案符合问题要求,而且他先回答了。
    • 您先生,救了我的命。我不知道如何只复制 cythonized 包的数据文件。一个小的改进,我从自定义的 build_ext 类中复制了 init.py 文件,而不是创建新的文件,以防它们中有逻辑。
    【解决方案3】:

    我建议您使用轮格式(如 fish2000 所建议的)。然后,在您的setup.py 中,将packages 参数设置为[]。您的 Cython 扩展程序仍将构建,并且生成的 .so 文件将包含在生成的 wheel 包中。

    如果您的 __init__.py 不包含在轮子中,您可以覆盖 Cython 提供的 build_ext 类的 run 方法并将文件从源代码树复制到构建文件夹(路径可以在self.build_lib)。

    【讨论】:

    • 有效!!我将使用 setup.py 更新答案以进行自我竞争。
    【解决方案4】:

    这正是the Python wheels formatdescribed in PEP 427 – 旨在解决的问题。

    Wheels 是 Python 鸡蛋的替代品(由于多种原因,这些鸡蛋有问题)–they are supported by pip,可以包含特定于架构的私有二进制文件(这里是 one example of such an arrangement),并且被 Python 社区普遍接受在这类事情中占有一席之地。

    这是aforelinked Python on Wheels 一文中的setup.py sn-p,展示了如何设置二进制分布:

    import os
    from setuptools import setup
    from setuptools.dist import Distribution
    
    class BinaryDistribution(Distribution):
        def is_pure(self):
            return False
    
    setup(
        ...,
        include_package_data=True,
        distclass=BinaryDistribution,
    )
    

    ...在您正在使用的较旧的setuptools 类的 leu 中(但可能仍以某种方式得到规范支持)。正如所概述的那样,为您的分发目的制作 Wheels 非常简单——我从经验中回忆起,wheel 模块的构建过程在某种程度上对 virtualenv 有所了解,或者它很容易在另一个中使用。

    无论如何,我认为,将setuptools 基于鸡蛋的 API 换成基于轮子的工具应该可以为您省去一些严重的痛苦。

    【讨论】:

    • 非常感谢您的回答。我仍然无法让它工作。听从了您的建议,但 .py 和 .pyc 文件仍包含在轮子中。并且找不到如何删除文件的子类(或任何需要的)分发类。我阅读了所有链接并注意到。关于您引用的示例,我不知道如何将该 yaml 配置文件转换为 python setup.py 脚本。 ¿ 你能提供更多细节吗?谢谢!
    • @eguaio 你应该看看 Delocate - github.com/matthew-brett/delocate - 它的源代码有很多有用的函数来操作轮子和共享对象文件。这可能会让你继续前进……至于 yaml 的事情,我预测这将是项目中最困难/令人恼火的部分,你只需要潜入并编写你自己的配置文件阅读器代码,hewn与您使用的数据所采用的内在形式非常接近。确实!
    • 我在指定的链接中进行了搜索,但仍然可以找到解决方案。在这些链接中,对 Distribution、run_setup、setup 等类、函数和模块进行了大量调整。我尝试使用 bdist_wheel 命令导航许多库的源文件,但源代码仍在分发中。
    • 非常感谢您的帮助,但恐怕您只提供了解决方案的指针,但对于我发布的小简单案例,我们仍然缺乏完整的独立解决方案。我无法使用您的指示来获得解决方案,这就是为什么我没有将您的答案标记为正确。
    猜你喜欢
    • 2021-04-22
    • 2010-12-23
    • 2021-06-01
    • 1970-01-01
    • 2015-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多