User experience with porting off setup.py

Any migration around packaging/building a library is difficult because it is hard to know how exactly downstream users/packagers are using the existing build scripts. A setup.py can do many different things and has a large array of configuration options that can be passed on the command line. It is difficult to know what possible python setup.py ... invocations are being used so it is difficult to anticipate whether or not any new configuration still satisfies all downstream use cases. It is also basically impossible to test this without just putting the sdist on PyPI and waiting for feedback at which point it is too late to go back and change the metadata.

There are two steps in a migration from setup.py to pyproject.toml. The first step is just adding pyproject.toml, specifying the setuptools backend and then moving some static metadata from either setup.py or setup.cfg to pyproject.toml but keeping all of the build code in place in setup.py. In principle this is straight-forward but there are many potential complications that could arise and it is basically impossible for a library author to anticipate them all (see
@henryiii’s comment here).

Actually removing the setup.py might be easy but the premise that everything from setup.py can be moved to pyproject.toml amounts to saying that the build configuration for the package can be specified completely statically which is not the case for many projects. A complicated setup.py typically tries to handle things like different compiler options and building in different ways and so on and so there are some things that are inherently not “static” or at least that cannot be easily expressed statically with the options available.

I don’t think that this is a particularly complicated case but python-flint’s setup.py currently has this:

if sys.version_info < (3, 12):
    from distutils.core import setup
    from distutils.extension import Extension
    from numpy.distutils.system_info import default_include_dirs, default_lib_dirs
    from distutils.sysconfig import get_config_vars
else:
    from setuptools import setup
    from setuptools.extension import Extension
    from sysconfig import get_config_vars
    default_include_dirs = []
    default_lib_dirs = []


libraries = ["flint"]


if sys.platform == 'win32':
    #
    # This is used in CI to build wheels with mingw64
    #
    if os.getenv('PYTHON_FLINT_MINGW64'):
        includedir = os.path.join(os.path.dirname(__file__), '.local', 'include')
        librarydir1 = os.path.join(os.path.dirname(__file__), '.local', 'bin')
        librarydir2 = os.path.join(os.path.dirname(__file__), '.local', 'lib')
        librarydirs = [librarydir1, librarydir2]
        default_include_dirs += [includedir]
        default_lib_dirs += librarydirs
        # Add gcc to the PATH in GitHub Actions when this setup.py is called by
        # cibuildwheel.
        os.environ['PATH'] += r';C:\msys64\mingw64\bin'
        libraries += ["mpfr", "gmp"]
    elif os.getenv('PYTHON_FLINT_MINGW64_TMP'):
        # This would be used to build under Windows against these libraries if
        # they have been installed somewhere other than .local
        libraries += ["mpfr", "gmp"]
    else:
        # For the MSVC toolchain link with mpir instead of gmp
        libraries += ["mpir", "mpfr", "pthreads"]
else:
    libraries = ["flint"]
    (opt,) = get_config_vars('OPT')
    os.environ['OPT'] = " ".join(flag for flag in opt.split() if flag != '-Wstrict-prototypes')


define_macros = []
compiler_directives = {
    'language_level': 3,
    'binding': False,
}


# Enable coverage tracing
if os.getenv('PYTHON_FLINT_COVERAGE'):
    define_macros.append(('CYTHON_TRACE', 1))
    compiler_directives['linetrace'] = True

Some of this is used for building wheels in CI and some of it is used for development work and some of it is used by downstream packagers like conda. The conda packages are built with MSVC on Windows and link to the MPIR library instead of GMP. The PyPI wheels are built with MinGW64 on Windows instead. There is even a CI script that dynamically generates a setup.cfg file to persuade cibuildwheel to use the MinGW compiler:

I am not at all happy with the way that all of this is configured with setuptools and cibuildwheel but somehow all of these different cases need to be handled. I just don’t see how to express all of this logic in pyproject.toml and I am sure that any change here would break something downstream.

2 Likes