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

发布于 2021-01-29 17:23:53

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

文件夹结构为:

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

我发现Cython可以做到这一点,方法是将.so库中的每个.py文件转换为可以直接用python导入的.so库。

问题是:setup.py文件必须怎样才能便于打包和安装?

目标系统具有virtualenv,必须使用允许轻松安装和卸载的任何方法安装软件包(欢迎使用easy_install,pip等)。

我尽了一切可能。我阅读setuptoolsdistutils记录了所有与stackoverflow相关的问题,并尝试了各种命令(sdist,bdist,bdist_egg等),并在文件条目中使用了setup.cfg和MANIFEST.com的许多组合。

我得到的最接近的是下面的安装文件,该文件将bdist_egg命令子类化,以便同时删除.pyc文件,但这破坏了安装。

如果覆盖了正确安装中包含的所有辅助文件(我需要pip freeze在venv中运行,请参见参考资料
mymod==0.0.1),那么“手动”安装venv文件的解决方案也不错。

使用以下命令运行它:

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_extclassrun方法,以使__init__.py文件进入车轮。

关注者
0
被浏览
179
1 个回答
  • 面试哥
    面试哥 2021-01-29
    为面试而生,有面试问题,就找面试哥。

    不幸的是,公认的设置答案packages=[]是错误的,并且可能破坏很多东西,例如可以在此问题中看到。不要使用它。而不是从dist中排除所有软件包,您应该仅排除将被cythonized并编译为共享对象的python文件。

    下面是一个工作示例;它使用我的食谱中的问题从python
    bdist_egg或bdist_wheel中排除单个源文件。示例项目包含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},
    )
    


知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看