只打包用 Cython 编译的 python 库的二进制编译 .so 文件
Package only binary compiled .so files of a python library compiled with Cython
我有一个名为 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 等都是
欢迎)。
我尝试了所有我力所能及的。我阅读了 setuptools
和 distutils
文档,
所有与计算器相关的问题,
并尝试了各种命令(sdist、bdist、bdist_egg 等),其中有很多
setup.cfg 和 MANIFEST.in 文件条目的组合。
我得到的最接近的是下面的安装文件,它将 class bdist_egg
命令也删除 .pyc 文件,但这会破坏安装。
在 venv 中安装 "manually" 文件的解决方案是
也很好,前提是包含在适当文件中的所有辅助文件
安装都涵盖了(我需要在 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
您可能会注意到,目标是 linux 64 位 python 2.7.
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_ext
class run
方法才能在 wheel 中获取 __init__.py
文件。
这正是这类问题 the Python wheels format – described in PEP 427 – 旨在解决。
轮子是 Python 鸡蛋的替代品(were/are 有很多问题)– they are supported by pip
, can contain architecture-specific private binaries (here is 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
类 的 leu 中(但可能仍以某种方式仍然受到规范支持)。正如概述的那样,为您的分发目的制作 Wheels 非常简单 - 正如我从经验中回忆的那样,wheel
模块的构建过程有点认识到 virtualenv
,或者在其他.
无论如何,我认为,将 setuptools
基于 egg 的 API 换成基于轮子的工具应该可以为您省去一些严重的痛苦。
我建议你使用wheel 格式(fish2000 建议)。然后,在您的 setup.py
中,将 packages
参数设置为 []
。您的 Cython 扩展仍将构建,并且生成的 .so 文件将包含在生成的 wheel 包中。
如果你的 __init__.py
没有包含在 wheel 中,你可以覆盖 Cython 提供的 build_ext
class 的 run
方法并从你的源中复制文件tree 到构建文件夹(路径可以在 self.build_lib
中找到)。
虽然打包成轮子绝对是您想要的,但最初的问题是关于从包中排除 .py 源文件。 @Teyras 在 Using Cython to protect a Python codebase 中解决了这个问题,但他的解决方案使用了 hack:它从对 setup()
的调用中删除了 packages 参数。这可以防止 build_py 从 运行 步骤确实排除了 .py 文件,但它也排除了任何您想要包含在包中的数据文件。 (例如,我的包有一个名为 VERSION 的数据文件,其中包含包版本号。)更好的解决方案是将 build_py setup 命令替换为仅复制的自定义命令数据文件。
您还需要如上所述的 __init__.py
文件。所以自定义 build_py 命令应该创建 __init_.py
文件。我发现编译的 __init__.so
在导入包时运行,所以只需要一个空的 __init__.py
文件来告诉 Python 该目录是一个可以导入的模块。
您的自定义 build_py class 看起来像:
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},
)
不幸的是, is wrong and may break a lot of stuff, as can e.g. be seen in 。不要使用它。您不应从 dist 中排除所有包,而应仅排除将被 cython 化并编译为共享对象的 python 文件。
下面是一个工作示例;它使用 from the question 。示例项目包含包含两个模块 spam.eggs
和 spam.bacon
的包 spam
,以及包含一个模块 spam.fizz.buzz
:
的子包 spam.fizz
root
├── setup.py
└── spam
├── __init__.py
├── bacon.py
├── eggs.py
└── fizz
├── __init__.py
└── buzz.py
模块查找在 build_py
命令中完成,因此您需要使用自定义行为对其进行子类化。
简单情况:编译所有源代码,不做任何例外
如果您要编译每个 .py
文件(包括 __init__.py
s),覆盖 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.bacon
、spam.eggs
和 spam.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},
)
我有一个名为 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 等都是 欢迎)。
我尝试了所有我力所能及的。我阅读了 setuptools
和 distutils
文档,
所有与计算器相关的问题,
并尝试了各种命令(sdist、bdist、bdist_egg 等),其中有很多
setup.cfg 和 MANIFEST.in 文件条目的组合。
我得到的最接近的是下面的安装文件,它将 class bdist_egg 命令也删除 .pyc 文件,但这会破坏安装。
在 venv 中安装 "manually" 文件的解决方案是
也很好,前提是包含在适当文件中的所有辅助文件
安装都涵盖了(我需要在 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
您可能会注意到,目标是 linux 64 位 python 2.7.
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_ext
class run
方法才能在 wheel 中获取 __init__.py
文件。
这正是这类问题 the Python wheels format – described in PEP 427 – 旨在解决。
轮子是 Python 鸡蛋的替代品(were/are 有很多问题)– they are supported by pip
, can contain architecture-specific private binaries (here is 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
类 的 leu 中(但可能仍以某种方式仍然受到规范支持)。正如概述的那样,为您的分发目的制作 Wheels 非常简单 - 正如我从经验中回忆的那样,wheel
模块的构建过程有点认识到 virtualenv
,或者在其他.
无论如何,我认为,将 setuptools
基于 egg 的 API 换成基于轮子的工具应该可以为您省去一些严重的痛苦。
我建议你使用wheel 格式(fish2000 建议)。然后,在您的 setup.py
中,将 packages
参数设置为 []
。您的 Cython 扩展仍将构建,并且生成的 .so 文件将包含在生成的 wheel 包中。
如果你的 __init__.py
没有包含在 wheel 中,你可以覆盖 Cython 提供的 build_ext
class 的 run
方法并从你的源中复制文件tree 到构建文件夹(路径可以在 self.build_lib
中找到)。
虽然打包成轮子绝对是您想要的,但最初的问题是关于从包中排除 .py 源文件。 @Teyras 在 Using Cython to protect a Python codebase 中解决了这个问题,但他的解决方案使用了 hack:它从对 setup()
的调用中删除了 packages 参数。这可以防止 build_py 从 运行 步骤确实排除了 .py 文件,但它也排除了任何您想要包含在包中的数据文件。 (例如,我的包有一个名为 VERSION 的数据文件,其中包含包版本号。)更好的解决方案是将 build_py setup 命令替换为仅复制的自定义命令数据文件。
您还需要如上所述的 __init__.py
文件。所以自定义 build_py 命令应该创建 __init_.py
文件。我发现编译的 __init__.so
在导入包时运行,所以只需要一个空的 __init__.py
文件来告诉 Python 该目录是一个可以导入的模块。
您的自定义 build_py class 看起来像:
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},
)
不幸的是,
下面是一个工作示例;它使用 spam.eggs
和 spam.bacon
的包 spam
,以及包含一个模块 spam.fizz.buzz
:
spam.fizz
root
├── setup.py
└── spam
├── __init__.py
├── bacon.py
├── eggs.py
└── fizz
├── __init__.py
└── buzz.py
模块查找在 build_py
命令中完成,因此您需要使用自定义行为对其进行子类化。
简单情况:编译所有源代码,不做任何例外
如果您要编译每个 .py
文件(包括 __init__.py
s),覆盖 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.bacon
、spam.eggs
和 spam.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},
)