使用SCons作为distutils的构建引擎

7
我有一个带有一些C代码的Python包,需要构建扩展(具有一些非常规的构建需求)。我使用SCons作为我的构建系统,因为它非常好用和灵活。
我正在寻找一种方法,用SCons编译我的Python扩展,并准备好与distutils一起分发。我希望用户只需键入setup.py install,就可以使用SCons编译扩展,而不是默认的distutils构建引擎。
一个想法是重新定义distutils中的build_ext命令,但我找不到详细的文档。
有什么建议吗?
4个回答

2
enscons包似乎是为了完成问题所需而设计的。使用它构建带有C扩展的软件包的示例在这里。您可以拥有一个基本的软件包结构,如下所示:
pkgroot/
    pyproject.toml
    setup.py
    SConstruct
    README.md
    pkgname/
        __init__.py
        pkgname.py
        cfile.c

在这个例子中,pyproject.toml 文件可能如下所示:

[build-system]
requires = ["enscons"]

[tool.enscons]
name = "pkgname"
description = "My nice packahe"
version = "0.0.1"
author = "Me"
author_email = "me@me.com"
keywords = ["spam"]
url = "https://github.com/me/pkgname"
src_root = ""
packages = ["pkgname"]

[tool.enscons]部分中,包含了许多熟悉的setuptools/distutils setup函数的内容。从这里复制,setup.py函数可能包含以下内容:

#!/usr/bin/env python

# Call enscons to emulate setup.py, installing if necessary.

import sys, subprocess, os.path

sys.path[0:0] = ['setup-requires']

try:
    import enscons.setup
except ImportError:
    requires = ["enscons"] 
    subprocess.check_call([sys.executable, "-m", "pip", "install", 
        "-t", "setup-requires"] + requires)
    del sys.path_importer_cache['setup-requires'] # needed if setup-requires was absent
    import enscons.setup

enscons.setup.setup()

最后,SConstruct 文件可能如下所示:
# Build pkgname

import sys, os
import pytoml as toml
import enscons, enscons.cpyext

metadata = dict(toml.load(open('pyproject.toml')))['tool']['enscons']

# most specific binary, non-manylinux1 tag should be at the top of this list
import wheel.pep425tags
full_tag = next(tag for tag in wheel.pep425tags.get_supported() if not 'manylinux' in tag)

env = Environment(tools=['default', 'packaging', enscons.generate, enscons.cpyext.generate],
                  PACKAGE_METADATA=metadata,
                  WHEEL_TAG=full_tag)

ext_filename = os.path.join('pkgname', 'libcfile')

extension = env.SharedLibrary(target=ext_filename,
                              source=['pkgname/cfile.c'])

py_source = Glob('pkgname/*.py')

platlib = env.Whl('platlib', py_source + extension, root='')
whl = env.WhlFile(source=platlib)

# Add automatic source files, plus any other needed files.
sdist_source=list(set(FindSourceFiles() + 
    ['PKG-INFO', 'setup.py'] + 
    Glob('pkgname/*', exclude=['pkgname/*.os'])))

sdist = env.SDist(source=sdist_source)
env.Alias('sdist', sdist)

install = env.Command("#DUMMY", whl, 
                      ' '.join([sys.executable, '-m', 'pip', 'install', '--no-deps', '$SOURCE']))
env.Alias('install', install)
env.AlwaysBuild(install)

env.Default(whl, sdist)

完成这些步骤后,您应该能够运行。
sudo python setup.py install

编译C扩展并构建wheel,然后安装python包;或者

python setup.py sdist

构建源代码分发。

我认为你可以在SConstruct文件中基本上做任何你能用SCons做的事情。


不要只贴链接作为答案,加上一些文字解释如何帮助OP解决当前的问题。谢谢。 - ρяσѕρєя K
@ρяσѕρєяK 我已经添加了一个更具体的例子,源自于这些链接。 - Matt Pitkin
我认为@joeforker是enscons的作者,所以他们或许可以对这个答案进行补充。 - Matt Pitkin

1

我的问题更多地涉及在setup.py脚本中以最清晰的方式编译Python扩展。通过这种方式,我可以轻松地分发我的应用程序。 - pygabriel
@pygabriel 编译C扩展不需要SCons。根本不需要。 - bialix
2
@bialix,这不是关于是否需要scons的问题,例如我已经有一个使用scons进行构建的项目...我该如何将其插入到distutils中,以便只需编写“python setup.py install”或开发和distutils只需调用scons。 - Loïc Faure-Lacroix
@Loïc Faure-Lacroix,你需要编写distutils扩展。该扩展将成为您项目的一部分。TS的假设是正确的:“重新定义build_ext命令”。关于build_ext的最完整文档是build_ext本身的源代码。 - bialix

0
解决方案是提供一个自定义的cmdclass,它派生自distutils.cmd.Command,并以您想要的方式构建模块:
import distutils.cmd

class build_py_cmd(distutils.cmd.Command):
    def initialize_options(self):
        pass

    def finalize_options(self):
        pass
    def run(self):
        print("Calling SCons to build the module")
        pbs.scons()


setup(name = 'riak3k',
      packages = ['riak3k'],
      package_dir = {'': 'build/release'},
      cmdclass = {'build_py': build_py_cmd},

pbs 导入是什么? - anatoly techtonik
看起来 pbs 模块是从 https://pypi.python.org/pypi/pbs 获取的,用于将操作系统函数作为 Python 函数调用。据我所知,它相当于 subprocess.check_call(['scons']) - Eldritch Cheese

0

我使用Scons生成setup.py文件。因此,我创建了一个名为setup.py.in的模板,并使用Scons扩展该模板以生成setup.py。

以下是执行此操作的项目链接:

setup.py.in template

The SConstruct

它会计算键值对字典,以替换setup.py.in模板以生成setup.py。

因此,最终用户需要执行两件事:

scons setup.py
python setup.py install

警告:我的sconstruct文件有点乱,因为我写了一段时间,但它应该可以演示这个概念。

如果你学会如何编写正确的scons工具,那么这可以转换为一个单一的目标,例如:

scons --pymod

这是一个使用SWIG生成Python包装器的scons工具:
import SCons.Action
from SCons.Script import EnsureSConsVersion

SCons.Script.EnsureSConsVersion(0,96,92)

SwigGenAction = SCons.Action.Action('$SWIGGENCOM', '$SWIGGENCOMSTR')

def emitter(target, source, env):
    """
    Add dependency from target to source
    """

    env.Depends(target, source)

    return target, source


def generate(env):
    """
    Add builders and construction variables for the SwigGen builder.
    """

    if 'SWIGCOM' not in env:
        raise SystemError("SCons build environment could not detect tool: swig")

    bld = env.Builder(
        action = SwigGenAction,
        emitter = emitter,
        target_factory = env.fs.File)

    env['BUILDERS']['SwigGen'] = bld

    env['SWIGGENCOM'] = env['SWIGCOM']


def exists(env):
    return env.Detect('swig')

现在你可以从SConstruct文件中生成包装器代码:

foobar_cc = env.SwigGen("foobar_wrap.cc", "foobar.i")

如果您修改了 foobar.i,它将重新生成 foobar_wrap.cc。一旦您拥有了这个文件,您可以编写其他工具来实际执行 python setup.py install,因此当提供 --pymod 时,它将构建 Python 模块。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接