Building a module with setuptools and swig

微笑、不失礼 提交于 2020-06-13 12:29:30

问题


I have a bunch of SWIG interfaces (foo.i, bar.i, etc.). I want to build them into a Python (3.6.4) module for my platform (MS Windows), with setuptools. The module should include the SWIG-generated Python files (*.py), the binary extensions (*.pyd) and the compiled caches (*.pyc). My setup.py is essentially like this:

from setuptools import setup, Extension
from pathlib import Path
paths=list(Path('.').glob('*.i'))
py=[path.stem for path in paths]
ext=[Extension('_' + path.stem, [str(path)]) for path in paths]
setup(py_modules=py, ext_modules=ext)

Now I build it with the following steps:

python setup.py build_ext -I..\include --swig-opts="-I..\include -c++" -b pkg
python setup.py build_py -c -d pkg
echo. > pkg\__init__.py

Using these steps, I get exactly what I want, under the pkg directory.

My question is: Is there no way to get this effect using a single invocation of setup.py, e.g., setup.py build? I think that build is supposed to call build_ext, but then I cannot see how to pass, e.g., the swig-opts option.

Update

Passing the SWIG options is solved (h/t @hoefling). The solution looks like this:

ext=[Extension(name='_' + path.stem,
            sources=[str(path)],
            swig_opts=['-I../include', '-c++'],
            include_dirs=['../include'])
            for path in paths]

However, with that layer of the onion peeled off, I can now see the layer below, which is this: setup.py build as a single invocation, wants to run the build_py first, and then build_ext afterward. Can you see why this fails? I have no Python sources. The Python scripts in my module are to be generated by SWIG, but SWIG doesn't get run until the build_ext step. Thus, I end up with the same question, which is how to build the module in a single invocation.

C:\some\path> python setup.py build
running build
running build_py
file foo.py (for module foo) not found
file foo.py (for module foo) not found
running build_ext
building '_foo' extension
swigging foo.i to foo_wrap.cpp
swig.exe -python -I../include -c++ -o foo_wrap.cpp foo.i
creating build
creating build\temp.win-amd64-3.6
creating build\temp.win-amd64-3.6\Release
cl.exe /c /nologo /Ox /W3 /GL /DNDEBUG /MD -I../include /EHsc /Tpfoo_wrap.cpp /Fobuild\temp.win-amd64-3.6\Release\foo_wrap.obj foo_wrap.cpp
Creating build\wib.win-amd64-3.6
link.exe /nologo /INCREMENTAL:NO /LTCG /DLL /MANIFEST:EMBED,ID=2 /MANIFESTUAC:NO /EXPORT:PyInit__foo build\temp.win-amd64-3.6\Release\foo_wrap.obj /IMPLIB:build\temp.win-amd64-3.6\Release\_foo.cp36-win_amd64.lib
Creating library build\temp.win-amd64-3.6\Release\_foo.cp36-win_amd64.lib and object build\temp.win-amd64-3.6\Release\_foo.cp36-win_amd64.exp
Generating code
Finished generating code

When complete, foo.py exists in the current working directory next to foo.i, and _foo.cp36-win_amd64.pyd exists under build\lib.win-amd64-3.6. (FYI, this transcript is edited slightly to protect proprietary information, e.g., I don't show all the full paths to all the echos of commands. And I did not include bar.i in the sources.)

To clarify:

  1. The error is this one: File foo.py (for module foo) not found

  2. Though all the required files exist in the output, except foo.cp36-win_amd64.pyc which I could possibly live without, I have to manually copy foo.py and _foo.cp36-win_amd64.pyd into some new directory to make a clean Python module.


回答1:


The problem is that once you declare py_modules, distutils includes build_py as subcommand in build and it is always being executed first because of the ordering of sub_commands list in build class. It simply doesn't know that SWIG will generate python module wrappers later in build_ext. I guess you can see this as a bug since one would expect distutils to be able to cope with generated modules, but meh.

Since you only have SWIG interfaces and all of your python modules are generated wrappers (so in your case build_ext is not dependent on build_py), you can change the ordering of subcommands in build so build_ext is executed first, then the rest. This will ensure that the python modules are generated before build_py is executed.

Basically, you need override the build command class and change the ordering of elements in sub_commands list. There are lots of ways to reorder a list in python; below is my proposal. For reordering the list I used the list conditional partitioning recipe from itertools recipes:

import itertools
from setuptools import setup, Extension
from distutils.command.build import build as build_orig


def partition(pred, iterable):
    t1, t2 = itertools.tee(iterable)
    return itertools.filterfalse(pred, t1), filter(pred, t2)


class build(build_orig):

    def finalize_options(self):
        super().finalize_options()
        condition = lambda el: el[0] == 'build_ext'
        rest, sub_build_ext = partition(condition, self.sub_commands)
        self.sub_commands[:] = list(sub_build_ext) + list(rest)


setup(
    ...,
    cmdclass={'build': build},
)



回答2:


Since the essential idea (see @hoefling's answer) is to subclass the build command to set its sub_commands property, this works:

from setuptools import setup, Extension
from distutils.command.build import build

class build_alt_order(build):
  def __init__(self, *args):
    super().__init__(*args)
    self.sub_commands = [('build_ext', build.has_ext_modules),
                         ('build_py', build.has_pure_modules)]

setup(py_modules=['foo.py', ...],
      ext_modules=[Extension('_foo', swig_opts=...), ...],
      cmdclass={'build':build_alt_order})


来源:https://stackoverflow.com/questions/50239473/building-a-module-with-setuptools-and-swig

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!