打包仅使用Cython编译的python库的二进制编译.so文件

时间:2016-09-14 20:51:13

标签: python cython setuptools distutils setup.py

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

文件夹结构为:

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

通过转换.so库中的每个.py文件,我发现Cython可以做到这一点 可以直接用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文件放在方向盘内。

4 个答案:

答案 0 :(得分:6)

虽然作为方向盘打包绝对是您想要的,但最初的问题是从包中排除 .py 源文件。 @Teyras在Using Cython to protect a Python codebase中解决了这个问题,但他的解决方案使用了hack:它从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},
)

答案 1 :(得分:3)

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

如果您的__init__.py未包含在方向盘中,则可以覆盖Cython附带的runbuild_ext方法,并将文件从源树复制到构建文件夹(路径可以在self.build_lib)中找到。

答案 2 :(得分:2)

不幸的是,设置为packages=[]的已接受答案是错误的,可能会破坏很多内容,例如在this question中可见。不要使用它。除了应该从dist中排除所有软件包之外,您还应该仅排除将被cythonized并编译为共享对象的python文件。

下面是一个有效的示例;它使用问题my recipe中的Exclude single source file from python bdist_egg or bdist_wheel。示例项目包含具有两个模块spamspam.eggs的软件包spam.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__.py),则覆盖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_package_modules(self, package, package_dir):
        modules = super().find_package_modules(package, package_dir)
        return filter(not_cythonized, modules)


setup(
    name='...',
    version='...',
    packages=find_packages(),
    ext_modules=cythonize(extensions, exclude=cython_excludes),
    cmdclass={'build_py': build_py},
)

答案 3 :(得分:1)

正是这种问题the Python wheels format - described in PEP 427 - 是为了解决而开发的。

轮子是Python鸡蛋的替代品(出于多种原因而存在问题) - they are supported by pip,可以包含特定于体系结构的私有二进制文件(此处为one example of such an arrangement),并且通常由拥有这些东西的Python社区。

以下是aforelinked Python on Wheels 文章中的一个setup.py代码段,展示了如何设置二进制分发:

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类的列伊中。根据经验,我可以非常直接地为您的分发目的制作轮子 - 正如我从经验中回忆的那样,wheel模块'构建过程有点认识virtualenv,或者很容易在setuptools内使用它。其他

在任何情况下,我应该考虑使用char基于鸡蛋的基于轮子的工具API进行交易可以为您节省一些严重的痛苦。