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

仅打包使用Cython编译的python库的二进制编译.so文件,python,cython,setuptools,distutils,setup.py,Python,Cython,Setuptools,Distutils,Setup.py,我有一个名为mypack的包,里面有一个模块mymod.py,并且 \uuuu init\uuuu.py。 由于一些不在讨论中的原因,我需要对编译后的模块进行打包 (不允许使用.py或.pyc文件)。也就是说,\uuu init\uuuu.py是唯一的 分布式压缩文件中允许的源文件 文件夹结构为: . │ ├── mypack │ ├── __init__.py │ └── mymod.py ├── setup.py 我发现Cython可以通过转换.so库中的每个.py文件来实现

我有一个名为
mypack
的包,里面有一个模块
mymod.py
,并且
\uuuu init\uuuu.py
。 由于一些不在讨论中的原因,我需要对编译后的模块进行打包 (不允许使用.py或.pyc文件)。也就是说,
\uuu init\uuuu.py
是唯一的 分布式压缩文件中允许的源文件

文件夹结构为:

. 
│  
├── mypack
│   ├── __init__.py
│   └── mymod.py
├── setup.py
我发现Cython可以通过转换.so库中的每个.py文件来实现这一点 可以使用python直接导入

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

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

我尽了我所能。我阅读了
setuptools
distutils
文档, 所有与此相关的问题, 并尝试了各种命令(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
正如您可能注意到的,目标是使用Python2.7的64位linux

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 )

关键点是设置
包=[],
。需要重写
build\u ext
class
run
方法才能将
\uuu init\uuuuuuuuupy
文件放在控制盘中。

这正是我们开发用来解决的问题

Wheels是Python卵的替代品(由于一系列原因,这些卵有问题)–它可以包含特定于体系结构的私有二进制文件(这里是),并且被在这类事情上有利害关系的Python社区普遍接受

下面是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,
)
…在您正在使用的较旧(但可能仍以某种方式受到规范支持)的leu
setuptools
类中。正如所述,为您的分发目的制作控制盘非常简单–正如我从经验中回忆的那样,
控制盘
模块的构建过程在某种程度上认识到了
virtualenv
,或者很容易在另一个控制盘中使用一个控制盘


在任何情况下,我认为,使用
setuptools
egg-basedapi进行基于轮子的工具交易应该可以为您节省一些严重的痛苦。

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


如果控制盘中没有包含您的
\uuuu init\uuuuuuuuuuupy
,您可以覆盖Cython提供的
build\u ext
类的
运行方法,并将文件从源代码树复制到build文件夹(路径可在
self.build\u lib
中找到)。

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

您还需要如上所述的
\uuuu init\uuuu.py
文件。因此,自定义build\u py命令应该创建
\uu init\upy.py
文件。我发现编译的
\uuuuu init\uuuuu.so
在导入包时运行,因此只需要一个空的
\uuuuuuu init\uuuuuuuuuy.py
文件就可以告诉Python目录是一个可以导入的模块

您的自定义生成类如下所示:

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()
并将安装程序配置为覆盖原始的build_py命令:

不幸的是,它是错误的,可能会损坏很多东西,例如,可以在中看到。不要用它。您不应该从dist中排除所有包,而应该只排除将被cythonize并编译为共享对象的python文件

下面是一个工作示例;它从问题中使用。示例项目包含包含两个模块的包
spam
spam.eggs
spam.bacon
,以及包含一个模块
spam.fizz.buzz
的子包
spam.fizz

root
├── setup.py
└── spam
    ├── __init__.py
    ├── bacon.py
    ├── eggs.py
    └── fizz
        ├── __init__.py
        └── buzz.py
模块查找是在
build\u py
命令中完成的,因此您需要使用自定义行为对其进行子类化

简单案例:编译所有源代码,不做任何例外 如果您要编译每个
.py
文件(包括
\uuuuu init\uuuuu.py
s),那么覆盖
build\u py.build\u包
方法就足够了,使其成为noop。由于
build\u包
没有任何作用,因此根本不会收集
.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扩展与源模块混合使用 如果你想要c
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},
)
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},
)