Search code examples
pythonsconsdistutils

Using SCons as a build engine for distutils


I have a python package with some C code needed to build an extension (with some non-trivial building needs). I have used SCons as my build system because it's really good and flexible.

I'm looking for a way to compile my python extensions with SCons ready to be distributed with distutils. I want that the user simply types setup.py install and get the extension compiled with SCons instead of the default distutils build engine.

An idea that comes to mind is to redefine build_ext command in distutils, but I can't find extensive documentation for it.

Any suggestion?


Solution

  • The enscons package seems to be designed to do what the question asked. An example of using it to build a package with C extensions is here.

    You could have a basic package structure like:

    pkgroot/
        pyproject.toml
        setup.py
        SConstruct
        README.md
        pkgname/
            __init__.py
            pkgname.py
            cfile.c
    

    In this the pyproject.toml file could look something like:

    [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"]
    

    where the [tool.enscons] section contains many things familiar to the setuptools/distutils setup functions. Copying from here, the setup.py function could contain something like:

    #!/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()
    

    Finally, the SConstruct file could look something like:

    # 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)
    

    After this you should be able to run

    sudo python setup.py install
    

    to compile the C extension and build a wheel, and install the python package, or

    python setup.py sdist
    

    to build a source distribution.

    I think you can basically do anything you could with SCons in the SConstruct file though.