Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project setup.cfg causes errors in cibuildwheel installing other packages #1487

Closed
rogerbinns opened this issue Apr 30, 2023 · 43 comments
Closed

Comments

@rogerbinns
Copy link

Description

My project is a C extension. Of note it has no dependencies on any other package for building or testing.

To customize the github actions binary build I added a setup.cfg to it with some additional options defined.

[build]
fetch = True
enable_all_extensions = True
enable = COLUMN_METADATA

This is causing an error when cibuildwheel is installing other packages because the current working directory is my project and my setup.cfg is affecting their installation. In particular the fetch option causes their installation to fail because they have no knowledge of it (and it is my option not theirs).

Here is an example of the problem occurring:

Installing build tools...
  
  + pip install --upgrade setuptools wheel -c 'C:\Program Files (x86)\pipx\.cache\f150de73f83b030\Lib\site-packages\cibuildwheel\resources\constraints-python36.txt'
  Requirement already satisfied: setuptools in c:\users\runneradmin\appdata\local\temp\cibw-run-6rvfifu1\cp36-win32\build\venv\lib\site-packages (59.6.0)
  Requirement already satisfied: wheel in c:\users\runneradmin\appdata\local\temp\cibw-run-6rvfifu1\cp36-win32\build\venv\lib\site-packages (0.37.1)
  ERROR: Exception:
[ ... frames elided ...]
  distutils.errors.DistutilsOptionError: error in setup.cfg: command 'build' has no such option 'fetch'
Error: Command ['pip', 'install', '--upgrade', 'setuptools', 'wheel', '-c', 'C:\\Program Files (x86)\\pipx\\.cache\\f150de73f83b030\\Lib\\site-packages\\cibuildwheel\\resources\\constraints-python36.txt'] failed with code 2. None

I did a second run where I changed the setup.cfg to not be present until just before the build so that it didn't affect the build packages being installed (using CIBW_BEFORE_BUILD). The test step then fell over:

Testing wheel...
  
      + pip install virtualenv -c /constraints.txt
  ERROR: Exception:
[ ... frames elided ...]
  distutils.errors.DistutilsOptionError: error in setup.cfg: command 'build' has no such option 'fetch'

I tried removing the setup.cfg in CIBW_BEFORE_TEST but that is run after the step above so it is too late.

This is the build log when CIBW_BEFORE_BUILD adds the setup.cfg so building works, but then falls over on the Testing wheel stage.

Suggested solutions:

  • cibuildwheel should not have its current working directory be my project while installing things it wants
  • add CIBW_AFTER_BUILD so I can delete the setup.cfg before test stuff is installed

Build log

https://github.com/rogerbinns/apsw/actions/runs/4844599878/jobs/8632966833

CI config

https://github.com/rogerbinns/apsw/actions/runs/4844599878/workflow

@joerick
Copy link
Contributor

joerick commented Apr 30, 2023

That way that you're using setup.cfg is quite unusual. Normally setup.cfg is a static file that lives with the project and is committed to source control. If you want to provide build-time parameters to your package, I'd recommend using environment variables- you can set them for this build using CIBW_ENVIRONMENT. (There might also be a way to use CIBW_CONFIG_SETTINGS for this too but I'm not sure how that all works in setuptools. )

@rogerbinns
Copy link
Author

It is a static file and it is part of the project! This issue would still apply - the intention surely can't be that the project setup.cfg should affect other non-project packages cibuildwheel is installing!

Some details about why and what is done:

APSW wraps the most recent SQLite library (source or binary), which means the most recent SQLite needs to be present at compile time. It has always had an option to automatically fetch the SQLite source code for you.

When someone is doing an install directly from the github source release they will need to choose how SQLite is found. For example they can supply the fetch flag to setup.py, put it in a sqlite3 directory, supply their own setup.cfg, or have it found on the system. All of those mechanisms are used. They also need a C compiler and the Python header files available.

For a pypi release it isn't practical to have all those mechanisms, and compilers + header files are unlikely to be present. So the decision is to have SQLite automatically fetched and have binary builds on pypi. There is also a source distribution on pypi and it should produce exactly the same results as installing a binary release.

To make a source pypi distribution I include a setup.cfg that does the appropriate options, because compilation is done on the users machine. For the binary builds I had been specifying stuff in environment variables etc for cibuildwheel.

That then led to a bug (and doubled maintenance burden), because the two different mechanisms got out of sync. So I made the binary build also use the same single setup.cfg, which led me to creating this issue.

@henryiii
Copy link
Contributor

henryiii commented Apr 30, 2023

setup.cfg is not intended to by supplied or edited by a user. It's a static file that allows you to move configuration out of your setup.py. It's also deprecated in favor of pyproject.toml configuration. What you are doing is setting "build-options", which is deprecated, at least from outside a file. AFAIK, both pip and setuptools are trying to abandon this old, setuptools-specific way of controlling things, partially due to the problems you are seeing (they are not scoped to a single project).

I'd highly recommend a) adding a pyproject.toml, with build-backend & requires set to ensure you are getting a modern isolated build. I'd recommend b) setting the default behavior to be ideal for most users, and then c) adding environment variables to allow users to control the rest.

I'm not sure how most users get your project, but often users are building from SDist, which does not even offer the opportunity to edit a project file like setup.cfg! If you really want to be able to configure the build, you need to switch to a backend that supports the modern --config-settings properly, like scikit-build-core, meson-python, maturin (Rust only), etc. Though I don't think any of those (yet) allow you to add arbitrary options.

By the way, I thought you said you have no dependencies, so what is failing to build? It has to be something that's not provided via wheel, why are you building someone else's wheels in your CI?

@henryiii
Copy link
Contributor

henryiii commented Apr 30, 2023

You have this in your config, and it's the line that's failing, I think:

CIBW_BEFORE_BUILD: python setup.py build

This is very much not supported. You should not build before you build. You need to use the built-in project builder (pip or pypa/build). Even if this did work it would build your project twice.

Also, calling python setup.py <stuff> was deprecated years ago.

@rogerbinns
Copy link
Author

Sorry, I am getting very confused by the responses. If you are saying that it is an error for a project to have a setup.cfg when using cibuildwheel, then close the issue, and ideally make cibuildwheel refuse to run if setup.cfg is detected.

I am the project developer. I am the one supplying the setup.cfg. There is no user involved.

The build process, setup.py, setup.cfg, etc all work and have all worked with cibuildwheel for ages. There is absolutely no problem there. I understand that this traditional distutils type approach is deprecated, but it still provides the most reliable path for building C extensions especially if some customisation is needed like me adding options to fetch SQLite, and configure what extensions in SQLite are enabled.

Everything was good until the other day when I moved some of those options out of CIBW variables and put them into a setup.cfg. This bug report is that the options I put in my setup.cfg for my setup.py (which works perfectly) now fail under cibuildwheel because my setup.cfg is being used when cibuildwheel is installing other packages.

The root cause of that is that cibuildwheel has the current working directory be my project when doing the other package installs.

If you want proof that this stuff all works with my project then do this, feeling free to replace the last line with an alternate build command. You will see that it does the fetch as directed by setup.cfg.

$ git clone https://github.com/rogerbinns/apsw.git
$ cd apsw
$ cp tools/setup-pypi.cfg setup.cfg
$ python setup.py build

The line you quote does not fail, and in fact wasn't reached because the install of other packages by cibuildwheel before it was reached failed because they tried to use my setup.cfg!

As mentioned I did a later update where CIBW_BEFORE_BUILD is used to copy the setup.cfg into place.

This is the workflow which does the build step just fine. Here it is on ubuntu and you can see the build step took about 90 seconds which is about right, and would have failed if the fetch flag had not been detected or used.

Please please note that by clicking on any of the build_binary steps that building works, and then cibuildwheel does the "Testing wheel..." bit and runs

 pip install virtualenv -c /constraints.txt

It is that command that is failing. The reason for the failiure is that pip install virtualenv is using my setup.cfg which virtualenv install doesn't understand!

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

Sorry, no, didn't mean setup.cfg was not supported - many of our projects have one. I'm just saying that setting build command options, save for a very small subset (bdist_wheel.universal is the only one I can think of) is very rarely done, and the whole command option system is not very good. Most of the configurability you'll find in setuptools is being used by setuptools itself in order to extend distutils, and it only simi-public. (I spent quite some time before I realized that you can't extend commands reliably via entrypoints because setuptools is using that already and it then becomes a random fight on who gets the overloaded entrypoint first!). Setting a command option you've added in a custom command that is overriding an existing one is likely to cause problems - it's not a common or recommended design, to the best of my knowledge. Before build modernization, I think you were really supposed to come up with new custom commands, which is now deprecated (since you shouldn't run them directly).

From what I understood, you changed from the (working) design to setup.cfg so that users could build your package and only edit or replace setup.cfg.

I (or one of us, probably will be able to look into it tomorrow) needs to understand what exactly is happening here, though. Since you say you have no dependencies, only our dependencies should be being installed, and they should all provide wheels, so you should never even trigger a build step, so nothing should even try to look at setup.cfg. And the local setup.cfg shouldn't affect other packages, except maybe in the case that a dependency is missing a pyproject.toml (or the deprecated setup_requires is used).

You should never break your local directory so someone can't run pip install <some_package> from it, though. So if you are doing that, you probably are not using setup.cfg correctly. Though the cibuildwheel test phase does not run in your package directly.

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

FYI, I can only find 500 files in all GitHub with [build] set in setup.cfg, and I don't recognize very many or any custom options in the first few. For comparison, there are 30K+ that configure bdist_wheel (and that's in theory only pure Python projects still supporting Python 2+3 via universal=true!).

Also, making sure we install our requirements in a non-project directory sounds fine. But I would like to understand why it’s affected and not just pulling wheels.

@joerick
Copy link
Contributor

joerick commented May 1, 2023

It is a static file and it is part of the project!

I meant that the existence of setup.cfg here is a form of build configuration, rather than static - at build time it is copied into place to configure the build. I only meant that env vars might be a better choice for build configuration.

This issue would still apply - the intention surely can't be that the project setup.cfg should affect other non-project packages cibuildwheel is installing!

I suppose I'd agree! But the issue is not with cibuildwheel. This config file affects distutils, a module that (until recently) is part of Python. I was curious so I added your config file to a project, and I couldn't get any of these to work on Python <=3.9: python -m venv, pip install -U pip, pip install pytest.

So, even if we did happen to install packages from outside your project root, you're gonna have a lot of problems with this approach.

@joerick
Copy link
Contributor

joerick commented May 1, 2023

But I would like to understand why it’s affected and not just pulling wheels.

I too was curious - this is the backtrace from a pip install-

(env) joerick@joerick5 ~/D/apsw-bea6820a208728e421b3e51b351bf317616bc155> pip install urllib3
Collecting urllib3
  Downloading urllib3-2.0.1-py3-none-any.whl (123 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 123.3/123.3 kB 3.8 MB/s eta 0:00:00
Installing collected packages: urllib3
ERROR: Exception:
Traceback (most recent call last):
  File "/Users/joerick/Downloads/apsw-bea6820a208728e421b3e51b351bf317616bc155/env/lib/python3.8/site-packages/pip/_internal/cli/base_command.py", line 169, in exc_logging_wrapper
    status = run_func(*args)
  File "/Users/joerick/Downloads/apsw-bea6820a208728e421b3e51b351bf317616bc155/env/lib/python3.8/site-packages/pip/_internal/cli/req_command.py", line 248, in wrapper
    return func(self, options, args)
  File "/Users/joerick/Downloads/apsw-bea6820a208728e421b3e51b351bf317616bc155/env/lib/python3.8/site-packages/pip/_internal/commands/install.py", line 449, in run
    installed = install_given_reqs(
  File "/Users/joerick/Downloads/apsw-bea6820a208728e421b3e51b351bf317616bc155/env/lib/python3.8/site-packages/pip/_internal/req/__init__.py", line 72, in install_given_reqs
    requirement.install(
  File "/Users/joerick/Downloads/apsw-bea6820a208728e421b3e51b351bf317616bc155/env/lib/python3.8/site-packages/pip/_internal/req/req_install.py", line 773, in install
    scheme = get_scheme(
  File "/Users/joerick/Downloads/apsw-bea6820a208728e421b3e51b351bf317616bc155/env/lib/python3.8/site-packages/pip/_internal/locations/__init__.py", line 249, in get_scheme
    old = _distutils.get_scheme(
  File "/Users/joerick/Downloads/apsw-bea6820a208728e421b3e51b351bf317616bc155/env/lib/python3.8/site-packages/pip/_internal/locations/_distutils.py", line 141, in get_scheme
    scheme = distutils_scheme(dist_name, user, home, root, isolated, prefix)
  File "/Users/joerick/Downloads/apsw-bea6820a208728e421b3e51b351bf317616bc155/env/lib/python3.8/site-packages/pip/_internal/locations/_distutils.py", line 80, in distutils_scheme
    i.finalize_options()
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/distutils/command/install.py", line 366, in finalize_options
    self.set_undefined_options('build',
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/distutils/cmd.py", line 286, in set_undefined_options
    src_cmd_obj = self.distribution.get_command_obj(src_cmd)
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/distutils/dist.py", line 868, in get_command_obj
    self._set_command_options(cmd_obj, options)
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/distutils/dist.py", line 910, in _set_command_options
    raise DistutilsOptionError(
distutils.errors.DistutilsOptionError: error in setup.cfg: command 'build' has no such option 'fetch'

It seems that pip still uses distutils, even when installing a wheel (speculating, but perhaps to get the install location?). Distutils loads the config file and raises the error.

@rogerbinns
Copy link
Author

There seem to be two simultaneous responses going on here.

1: Don't use setup.cfg, use toml files, use environment variables, etc etc which will make this issue not be hit. I agree that would be the case. However please give me the benefit of the doubt, that there are good reasons why things are done the way they are and they do work. I would be delighted to continue this specific conversation somewhere more appropriate, but it is NOT relevant to this issue being reported here - it is an avoidance of the issue.

2: A setup.cfg in the project directory has no effect on cibuildwheel installing other software

The second one is wrong - it does and what this issue is about. I have created a minimal standalone test case at https://github.com/rogerbinns/issue1487

It adds --marker-sentinel to setup.py build command

Here is the clickable build run https://github.com/rogerbinns/issue1487/actions/runs/4851042323/
and as a text file

Line 419 has cibuildwheel running Testing wheel... and doing

pip install virtualenv -c /constraints.txt

Which fails because (backtrace ends line 448)

distutils.errors.DistutilsOptionError: error in setup.cfg: command 'build' has no such option 'marker_sentinel

This proves that the setup.cfg in the project directory is affecting the pip install of virtualenv.

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

IMO, you should not break pip install in your current directory. This happens to work for you because you don't have any dependencies. As soon as you add a single dependency like pytest, pip install . on your package will break, because it won't be able to install your dependency to get your package.

I didn't doubt that you are seeing this failure on adding setup.cfg, my point is you are putting things in setup.cfg that are unsupported due to their destructive side effects. There are a few things that work fine in setup.cfg, like setting up project metadata and setting a single setting on bdist_wheel. But configuring modified existing commands is not one of them. I don't really understand why this breaks, and I'd like to - you don't need setuptools/wheel installed to use pip (in fact, the latest virtualenv no longer installs them by default!).

I think changing the cwd of this command isn't terrible, as it doesn't need to be run in the project directory, so IMO it's probably okay just to do that. But in general, "thread 1" is important here, because we should not be doing lots of workarounds for something that is not a good practice and not supported by setuptools. If you require workarounds to do basic things like pip installing in the project directory, I think it's quite fair to say you need to do workarounds to make cibuildwheel work too, rather than making us implement the workarounds in cibuildwheel. Also a little worried about how we'd test it - Windows/macOS do a lot more installing, and I'm not sure how we should detect that we missed one. Maybe your MWE could be added to our test projects if there's an easy one to add it to. But as long as it's the only workaround, it doesn't seem too bad.

Also, the suggestion that you add the following pyproject.toml:

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_api"

Is a general one and not related to the files you have. It should be done on all projects that today assume setuptools and wheel will be pre-installed (this is changing, actually already has changed in virtualenv as of a few days ago), and it ensures your build procedure will not change depending on pip version. And it ensures you'll always have wheel when building wheels, even if this is listed as a dependency in someone else's project.

@rogerbinns
Copy link
Author

I am happy to test potential fixes like PR1488 - let me know when (and probably how).

Some other potential (simpler?) fix approaches:

  • Do the install of virtualenv in the "Setting up build environment" phase
  • Run CIBW_BEFORE_TEST before the virtualenv install in "Testing wheel"

The real gotcha is pip install using setup.cfg in the current directory to install content not from the current directory.

Partial thread 1 discussion:

  1. The existing build step works fine through all of this and produces working wheels and other bdist formats suitably configured, including building using cibuildwheel, binary from pypi, source from pypi, source from github release, source from git, across Python 3.6+ (including 3.12 alpha), all the platforms, using pip, invoking setup.py directly etc
  2. The project has been going almost 20 years and still has no dependencies on purpose (except stdlib)
  3. I moved fetch from outside of setup.cfg into setup.cfg to avoid duplication of configuration because that was causing problems where multiple configuration mechanisms got out of sync with each other when they should agree, so that setup.cfg is now the only mechanism used
  4. Nothing is actually broken (except when cibuildwheel runs in the project directory pip installing other stuff that then hits the setup.cfg gotcha)
  5. Doing a default build of Python 3.12a7 still installs setuptools. Since distutils got subsumed into setuptools, this means setup.py is still supported

Adding a pyproject.toml means addressing all these points!

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

The test environment phase shouldn't happen inside the build environment - it's optional, and that's mixing steps. The most common line in CIBW_BEFORE_TEST is probably pip install -r test_requirements.txt1 or similar - you can't do that before making the environment.

1). Up until setuptools is removed from the default environment, which is already happening. The first step happened 4 days ago with virtualenv v20.23.0. Expect it to continue to disappear from default environments.
2) My point is that projects should have dependencies, or at least the option to add them, and your design breaks the ability to add even a single dependency.
3) I suspect there's another less problematic and less deprecated way to do this.
4) I can't even run pip install pytest inside your project directory if this file is present, I'm not sure that "nothing is broken" counts here?
5) "Doing a default build of Python 3.12a7 still installs setuptools". I don't know what this means. Environments usually default to including setuptools (but that's changing, as I said). setup.py is setuptool's dynamic configuration file and always will be, that's not changing.

Adding a pyproject.toml doesn't require addressing anything, it simply informs tools that were updated on or after 2016 that setuptools is used and lets them use modern isolated builds. That's it, it doesn't "change" setup.cfg, setup.py, or MANIFEST.in. When setuptools is removed from the default environments, projects with a pyproject.toml will continue to work, and those without it will break (as they do now, but it's not as common).

(And I'm pretty sure it won't cause this to start working, as the isolation won't affect pip install other-package).

Footnotes

  1. https://github.com/search?type=code&auto_enroll=true&q=path%3A.yml+CIBW_BEFORE_TEST

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

You can test the PR now if you want. Take a line like
https://github.com/rogerbinns/apsw/blob/81f8fb23275fd834e94e6426dd470b557285356c/.github/workflows/build-testpypi.yml#L28
and replace the repo and tag with henryiii/cibuildwheel@henryiii/fix/installcwd.

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

How did you get that failure outside of cibuildwheel, @joerick? I am not seeing it in https://github.com/henryiii/issue1487. I'd have expected https://github.com/henryiii/issue1487/blob/2be7d04d9511ba24eeab4cd815b0135cc0044f6c/.github/workflows/build.yml#L39 to break.

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

Ahh, never mind, missed the Python < 3.9 part. Got the failure! So this is only an issue with 3.6-3.8.

So this is an example of a something you can't do (before Python 3.9) as soon as that setup.cfg is present:

on:
  workflow_dispatch:
  pull_request:
  push:
    branches:
      - main

jobs:
  build_local:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: "3.8"
      - run: pip install virtualenv

https://github.com/henryiii/issue1487/blob/d8c40f7946e045cdfa3dd148bd6817921ea23b5f/.github/workflows/build.yml

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

FYI, if you "fix" the test step by running it as in #1488, then the next line fails:

pip install /tmp/cibuildwheel/repaired_wheel/custom-1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Since pip can't do anything if this is set, even install a local wheel. So it's not really a fix. It might make the workaround a bit easier, since you can now delete the copied setup.cfg file in the CIBW_BEFORE_TEST step, but that's all.


This design also stops you from adding a pyproject.toml! As soon as you do, then you can control what is present. But pip needs to be able to download whatever you ask for ("setuptools", in this case), which then breaks, because pip can't download anything in the project directory with this setup.cfg present. (Actually, I think it may even break before it gets to "setuptools", since it's basically running python -m venv to create the building environment).

I believe the correct way to do set this, rather than coping around a setup.cfg file, would be to pass --build-option's, but that's currently a warning in pip and will be removed in pip 23.3.

IMO, setuptools did not support this added build-options in setup.cfg based design before Python 3.9, and they have been moving away from these sort of options (which is also why you don't see it used in other projects). (@abravalheri can correct me if I'm wrong?) Can't you just make these three options environment variables that cibuildwheel sets?

@rogerbinns
Copy link
Author

@henryiii some responses

1). Up until setuptools is removed from the default environment, which is already happening. The first step happened 4 days ago with virtualenv v20.23.0. Expect it to continue to disappear from default environments.

That is ok and something I will deal with as it happens. Please note that cibuildwheel is not the only or even the most common way the project is built. To my knowledge the only person who uses cibuildwheel is me once per release in order to publish on pypi.

Everybody else is invoking setup.py with appropriate options for their situation. There are many options covering how SQLite is found and its configuration, with no two using the same options, all of which matter at compile time.

  1. My point is that projects should have dependencies, or at least the option to add them, and your design breaks the ability to add even a single dependency.

And they can. Using standard releases from github do not have a setup.cfg at all. Doing a pip install of a source release from pypi does have the setup.cfg, but you aren't installing things into the current directory of the extracting source while pip does it build etc steps.

  1. I suspect there's another less problematic and less deprecated way to do this.

This has been an issue for years! Python has not yet provided a better means for building C extensions and letting the people who do the build provide options. Even the documentation still shows it done this way. setup.cfg is the only way to provide options to the various setup.py stages in the situation where no explicit command line parameters are provided. If there is a better I'm all ears!

  1. I can't even run pip install pytest inside your project directory,

Yes you can :-) There is no setup.cfg - look. If you grab a source release then there is a setup.cfg not made by me with two lines about eggs.

I'm not sure that "nothing is broken" counts here?

The only build that is broken is using cibuildwheel because when a setup.cfg is added just before build, the later pip installs done by cibuildwheel are done in my project directory.which picks up the setup.cfg not intended for them.

The other builds involve all the Linux and BSD distros, various other developers who want different customisations and options, those doing a pip install from pypi where there isn't a binary for their platform, and my testing which involves all the combinations of SQLite versions, 32 & 64 bit, all the Python versions in debug and release, Windows, etc.

  1. "Doing a default build of Python 3.12a7 still installs setuptools".
    I don't know what this means.

Just that the in development version of Python still installs setuptools - ie it hasn't actually been removed from "Python", yet.

Adding a pyproject.toml doesn't require addressing anything

It means I have to test it actually works across the matrix of all the releases, tools, versions etc. And will have no effect on this specific issue of setup.cfg. BTW PEP 518 has wording about the file and assuming what is there. It doesn't appear to be superseded.

@rogerbinns
Copy link
Author

You can test the PR now if you want

Thank you for making the changes. Here is the build doing it in the issue1487 repository I made earlier.
buildlog.txt

Progress - the "Testing wheel" stuff now proceeds past the pip install virtualenv part. However things fall over when trying to install the built wheel. Again it is the same issue - the pip install is run in my project directory to grab the wheel from elsewhere and the setup.cfg is being applied to some other setup.py which falls over. Having the working directory be the venv or similar should address that.

May I suggest also including the working directory each command cibuildwheel is run in is made part of the log.

@henryiii
Copy link
Contributor

henryiii commented May 1, 2023

Python has not yet provided a better means for building C extensions

This is exactly the point of pyproject.toml and removing setuptools as the one and only way to make Python projects. Other tools have been popping up with much better support for compiled extensions. But we can't move forward if people will not use pyproject.toml and specify what backend they want to use.

The only build that is broken is using cibuildwheel because when a setup.cfg is added just before build

My point is, if you did put this file in, it would break pyproject.toml builds, virtualenvs, and all pip install of anything, including your own package. The only reason it's working is because it's not in any build except cibuildwheel. You are copying a file that breaks all these things into cibuildwheel and then complaining that it breaks cibuildwheel.

hasn't actually been removed from "Python", yet

It's not in Python. Python does not contain setuptools. It contains "ensurepip", which installs pip and setuptools. It also contains venv, which includes wheel and setuptools by default, but that can be deactivated with --without-pip. Virtualenv has just dropped setuptools, so I would expect things to continue to move forward in other places too. Rye doesn't include setuptools in venvs at all. Etc.

And will have no effect on this specific issue of setup.cfg.

It actually completely breaks it, because it requires that venv and pip not be broken in the project directory. :)

Pip may change to using modern builds by default as soon as 23.3. More likely probably a version or two after that, though.

@rogerbinns
Copy link
Author

Your changes also mean CIBW_BEFORE_TEST can now remove the setup.cfg, and a build test in my issue1487 works perfectly.

This means that your changes are sufficient for me to work around my setup.cfg being picked up by pip install of other stuff that it shouldn't apply to. I do still believe that commands like pip install should not be run with the cwd being the project because files (like setup.cfg) can affect that install in hard to detect and certainly unwanted ways.

I will now do a build on my main project to do a full test. It takes many hours because of all the platforms.

@rogerbinns
Copy link
Author

These are my needs for building my C extension

  • The option to run fetch before building which downloads SQLite etc.
  • The ability to specify additional C flags (#defines)
  • The ability for Python code to check those C flags are correct / not contradictory etc
  • The ability to provide a default set of options (eg fetch on or off, various flags) when cibuildwheel is run
  • The ability to provide the same set of options when sdist is run, such that a user running pip install of the sdist from pypi has them applied automatically
  • Not maintaining those same set of options in multiple duplicate formats / files
  • A standard source release has no default set of options (ie user must choose)
  • Documentation telling people how to specify the options / fetch they want.
  • Works on all currently supported Python versions, and 3.6.

That is how I end up at the least worst solution of setup.py and setup.cfg. I would be delighted if something better exists ...

@rogerbinns
Copy link
Author

I got bitten by the os.abspath thing too, but you'd fixed it as I looked.

On retrying I got daring and added the pyproject.toml. That then caused a build failure because the pip installs are done with my project as the current working directory which picks up the setup.cfg which then gets applied to a package outside of my project which then fails.

This is what you were describing above.

Can't you just make these three options environment variables that cibuildwheel sets?

Yes I can easily do that, and it is what I was doing.

However if someone does a pip install of the source distribution from pypi it needs to have the same configuration. ie a binary or source install from pypi should have the same options established. The only way I could make that the case for the source distribution is by including a setup.cfg inside which is what I did.

That then resulted in having to maintain the exact same information twice in two different formats - once in the github workflow environment variables and again in the setup.cfg. Those ended up unintentionally diverging, so I went for only having it in the setup.cfg. Which then led me to discovering this issue.

The root problem is really that pip install is using setup.cfg from the current directory when downloading an external package and installing it somewhere else - ie the current directory is not relevant and should have no effect. I tried to make a reproducer in a standard venv and at no point did setup.cfg in the cwd have any effect. So this pip install using setup.cfg thing seems to only happen when using cibuildwheel.

@henryiii
Copy link
Contributor

henryiii commented May 2, 2023

I have a reproducer outside cibuildwheel at https://github.com/henryiii/issue1487, based on yours. You need Python <3.9 to see the issue.

@henryiii
Copy link
Contributor

henryiii commented May 2, 2023

You shouldn't and can't get rid of setup.py (without completely writing a new build system, like CMake/Meson, which also would probably require Python 3.7+ as we don't support 3.6 and this is all fairly new development). But using setup.cfg for this is not the way setup.cfg is normally used, and breaks on Python 3.6, 3.7, and 3.8. What about adding a new section to setup.cfg, like tool:apsw:build or something, and then reading your options (manually in the setup.py) from there? (You can also do this with tool.apsw.build in pyproject.toml, but then you need a dependency (tomli, though you can vendor it) for Python < 3.11, and I'm guessing you don't want much in this file except the custom setup). You could also add a new custom file, like apsw_build.cfg. You really don't want this shared with other config - for example, you can move most of the static config (like name, version, and such) to setup.cfg. But I think you really want a file with only a few defaults.

@henryiii
Copy link
Contributor

henryiii commented May 2, 2023

Could you try #1488 again?

@rogerbinns
Copy link
Author

A build I did yesterday afternoon (California time) ended up timing out. I started another two hours ago from this message and am having the similar problems. However the cibuildwheel bits relevant to this issue are working fine.

What I was trying to make a reproducer for is pip install using a local setup.cfg when installing a remote package, since that is the root cause of this issue. In particular when I try from outside of cibuildwheel the local setup.cfg is ignored, so something somewhere is causing the behaviour difference.

@rogerbinns
Copy link
Author

My build completed which includes all the platforms plus qemu ones, all the Python versions, running my tests, and publishing to (test) pypi. Looks good to me.

@rogerbinns
Copy link
Author

The issue occurs in Python 3.9 and earlier and is triggered by code in pip. I have two possible fixes.

  1. Adding 5 lines in pip itself
  2. Adding one line to the sysconfig module that is in the Python images used by cibuildwheel

@henryiii I'd appreciate your advice on how to proceed

Short description of the problem:

pip wants to get the list of all possible install directories which ends up instantiating a distutils install object which instantiates an unused distutils build object which doesn't understand build options from the setup.cfg in the current directory.

First fix

This creates a dummy distutils build command that accepts all options. Changes are to
https://github.com/pypa/pip/blob/main/src/pip/_internal/locations/_distutils.py adding this

class dummy_build(DistutilsCommand):
    def __getattr__(self, name):
       return []
    initialize_options = finalize_options = lambda *args: None

Then in distutils_scheme add the middle line:

    d = Distribution(dist_args)
    d.cmdclass["build"] = dummy_build # override default build
    if not ignore_config_files:

This fix means the setup.cfg file is still used, but only the install section.

Second fix

Append this line to the end of the sysconfig module in the cibuildwheel Python images for Python 3.9 and earlier.

_PIP_USE_SYSCONFIG = True

This fix means that setup.cfg and distutils is not used at all.

Long description of the problem:

The exception is happening when pip wants to know if all package directories are writable, long before doing the actual install.

Package directories are found by calling get_lib_location_guesses which calls get_scheme.

get_scheme looks for sysconfig._PIP_USE_SYSCONFIG, defaulting to True for Python 3.10+, False for earlier.

If sysconfig._PIP_USE_SYSCONFIG is True this issue does not occur as distutils is ignored.

If it is False, pip/locations/_distutils.py:distutils_scheme() is invoked.

distutils_scheme does:

  1. Creates a distutils.core.Distribution
  2. Gets the install command object
  3. Calls finalize_options on the install command object, which always creates distutils.command.build.build which then chokes on unknown options to build (this underlying issue)

I don't think the build section of a project specific setup.cfg should have an effect on pip finding install directories!

@henryiii
Copy link
Contributor

henryiii commented May 4, 2023

I don't think the build section of a project specific setup.cfg should have an effect on pip finding install directories!

The ship sailed on this long ago - it should have been fixed in pip before they dropped support for Python 3.6, and they are dropping support for 3.7 soon. Placing custom additions in [build] in setup.cfg is going to break a lot more than cibuildwheel, so cibuildwheel patching the problem will just push this off onto others. You only happen to be skirting around it by a) avoiding an isolated build (for now) by not providing a pyproject.toml and b) copying the setup.cfg in during the build process then removing it. Placing a static setup.cfg like this in the project breaks all usage of pip and virtual environments from the project directory.

The "fix" in #1488 doesn't seem too bad as it just isolates our extra package install steps and doesn't patch anything.

I would highly, highly recommend you reconsider your design; it's not valid until you drop Python 3.9. If pip does accept a patch, then it might become valid for the latest pips + 3.7+ or 3.8+, depending on if there's a release with it in it for 3.7. Most packages have managed without adding extra [build] arguments. Adding these sorts of setuptools arguments is discouraged AFAIK in favor of modern --config-settings.

@henryiii
Copy link
Contributor

henryiii commented May 4, 2023

Excellent, will watch that pip issue.

FYI, setuptools was just removed from Python 3.12's ensurepip and venv two weeks ago in python/cpython#101039. So 3.12 will truly be setuptools and distutils free. :)

@rogerbinns
Copy link
Author

How exactly are C extensions supposed to be built without setuptools and its embedded copy of distutils? Is this just a case of setuptools not being installed by default, or completely gone? The doc still has setuptools.

My original problem is that I want to specify build parameters in one place only. I had it in a setup.cfg for the source distribution that goes to pypi and as command line flags in the github action, and it was a source of bugs because they easily get out of sync.

I'm investigating another approach now, where the idea is to specify the command line flags in the github action, and then generate a setup.cfg from that, that is used when making the sdist.

However I can't find any documentation or examples of how to specify options to cibuildwheel that are then seen by setup.py. Even my workflow above is doing it in the CIBW_BEFORE_BUILD command which is the wrong place. Still looking.

@Czaki
Copy link
Contributor

Czaki commented May 4, 2023

How exactly are C extensions supposed to be built without setuptools and its embedded copy of distutils

setuptools will not be shipped by default. But it sill could be downloaded if needed.

@rogerbinns
Copy link
Author

I am completely stalled. The only way to pass options to setup.py build_ext is to pass them in a setup.cfg. I also need one option passed to build. This works perfectly except for pip install looking at the [build] section, so I'm stuck waiting for pip to be fixed and the current setup.cfg swapping. It also prevents me from having a pyproject.toml.

I couldn't find any way of passing the options on the command line via --config-setting machinery. Using the pyproject.toml wouldn't work either, because I have multiple configs depending on who the build is for, including no config at all.

If pip is not fixed, my last resort will be monkey patching https://github.com/pypa/setuptools/blob/main/setuptools/command/setopt.py#L19 to make it look for a different file. Yuck.

@joerick
Copy link
Contributor

joerick commented May 6, 2023

Let's put the setup.cfg stuff to one side. in terms of suggesting solutions here, can we go back to first principles?

My understanding is that you want to have different build settings between source checkouts, wheels builds and sdists, with source checkouts the odd-one-out. What are the specific options that you're setting, and what is the software in the build process that is reading these options?

@rogerbinns
Copy link
Author

@joerick I have implemented a workaround by monkey patching distutils, which does work going back to Python 3.7 (distutils still in stdlib) and Python 3.12a7 (distutils buried inside setuptools). That looks for a different filename but otherwise has the same semantics as setup.cfg. I will know in about 6 hours if everything is good.

To help with the understanding:

My project consists primarily of C code gluing the C api of CPython with the C api of SQlite. In order to build the project, the standard pattern of a setup.py with ext_modules pointing at the C code is used.

Various bits of information are needed at compile time, which includes compilation flags, debugging, whether optional functionality in SQlite is present etc. It also requires the current(ish) version of SQLite, which is unlikely to be present on the platform.

To address that my setup.py adds some extra commands such as fetch which downloads the SQLite source, and subclasses some others like build_ext to add and verify extra compilation flags. Nothing sophisticated is being done in that code.

The extension is built by the various Linux/BSD platforms, by developers who use it in their projects (sometimes including their own extra code at compilation time), and by me for publishing binary builds and source to pypi, and by me for source github releases.

In the olden days they would invoke it like python setup.py fetch --version=1.2.3 build_ext --debug --omit=fts5 install test specifying the commands (eg fetch) and flags (eg omitting SQLite features). Some would also provide their own setup.cfg to supply some of the the options.

setup.cfg has the restriction that you can't tell it to run the sub commands (eg fetch above), although you can provide parameters to the sub command if it is invoked. The various tools for Python packaging have moved away from being able to specify a rich setup.py command line like the example, so the only way of providing the needed configurability is was for me to attach it to the sub commands that are always run build and build_ext. All was good for everyone.

For pypi builds (using cibuildwheel) I had to pick a specific set of options, such as embedding a specific version of SQLite inside the extension, and which extensions are enabled/disabled. It is also important to me that if you get a binary build from pypi, the source build produces the same result with the same configuration. ie if there wasn't a binary build for your platform, the source based build pip does gives you the same configuration and options.

This is how configuration was done:

  • pypi binary builds: I specified the setup.py command line flags in the workflow
  • pypi source distro: I embedded a setup.cfg in the sdist which is obeyed when pip downloads and builds it
  • github source: No configuration present
  • Maintainers, developers: Take github source release, and provide configuration via the command line and their own setup.cfg

This issue arose because I had a bug in pypi installs where the configuration I specified in the workflow got out of sync with the setup.cfg. I fixed it by making both use the same setup.cfg so they would always be in sync. And then fell into this issue where my project setup.cfg was causing cibuildwheel and pip trying to install other stuff interpreting contents from a section it didn't need from some ancient distutils code side effects only trying to work out if --user should be automagically applied by pip!

@joerick
Copy link
Contributor

joerick commented May 8, 2023

The options that you set in setup.cfg, are they setuptools options or are they configuring your own code?

@rogerbinns
Copy link
Author

Some are for my own code (eg fetch related stuff, omitting/including functionality) and others are generic distutils/setuptools options.

This is the setup.py setup() call where you can see the metadata, and cmdclass= at the end for the added/augmented sub commands.

@joerick
Copy link
Contributor

joerick commented May 8, 2023

All I can see in the repo are is this config

[build]
fetch = True
enable_all_extensions = True
enable = COLUMN_METADATA

All of those appear to be read using this code in a subclass of a build command in setup.py

# We allow enable/omit to be specified to build and then pass them to build_ext
build_enable = None
build_omit = None
build_enable_all_extensions = False

bparent = build.build


class apsw_build(bparent):
    user_options=bparent.user_options+\
                  [ ("enable=", None, "Enable SQLite options (comma separated list)"),
                    ("omit=", None, "Omit SQLite functionality (comma separated list)"),
                    ("enable-all-extensions", None, "Enable all SQLite extensions"),
                    ("fetch", None, "Fetches SQLite for pypi based build"),
                    ]
    boolean_options = bparent.boolean_options + ["enable-all-extensions", "fetch"]

    def __init__(self, dist):
        self._saved_dist = dist
        bparent.__init__(self, dist)

    def initialize_options(self):
        v = bparent.initialize_options(self)
        self.enable = None
        self.omit = None
        self.enable_all_extensions = build_enable_all_extensions
        self.fetch = False
        return v

    def finalize_options(self):
        global build_enable, build_omit, build_enable_all_extensions
        build_enable = self.enable
        build_omit = self.omit
        build_enable_all_extensions = self.enable_all_extensions
        if self.fetch:
            fc = fetch(self._saved_dist)
            fc.initialize_options()
            fc.all = True
            fc.finalize_options()
            fc.run()
        return bparent.finalize_options(self)

So I'm wondering - rather than configuring these using the setuptools options system, can you read defaults for these options from somewhere else? e.g. you could have a build-defaults.json file and read it in setup.py like

import json
with open('build-defaults.json') as f:
    build_defaults_json = json.load(f)
build_enable = build_defaults_json['enable']
build_omit = build_defaults_json['omit']
build_enable_all_extensions = build_defaults_json['enable_all_extensions']

The reason I'm suggesting this is I think the core of your issue with setup.cfg is the creation of new options - putting the options elsewhere would stop distutils from choking on them.

@henryiii
Copy link
Contributor

henryiii commented May 8, 2023

You can even put it into setup.cfg, you just need a custom section (like PyTest, mypy, etc use) rather than changing the options of [build].

@henryiii
Copy link
Contributor

henryiii commented May 8, 2023

Though I think another file is better, as a setup.cfg is supposed to be static and contain permanent options (like most of the options you have in setup.py now), not something a user copies in from a tools dir.

@rogerbinns
Copy link
Author

@joerick

The reason I'm suggesting this is I think the core of your issue with setup.cfg is the creation of new options - putting the options elsewhere would stop distutils from choking on them.

I understand where you are coming from and thank you for doing the investigation. My recent bug was due to having two different ways of configuring the same thing, and then getting out of sync. Adding another way, including debugging and testing of it is something I'm avoiding :)

@henryiii

You can even put it into setup.cfg ...

I actually want the exact semantics that setup.cfg already provides, and don't want to have to reimplement the code in "distutils" that automatically loads the right options from the right sections and applies them to the right commands in the right way!

... setup.cfg is supposed to be static and contain permanent ...

It does for the environment where a build is happening. Builds using cibuildwheel for pypi done by me use the file I have, while other developers who build for their own environments have their own.

The defaults in setup.py do nothing, and it is almost certainly the case that trying to do a build using just those would fail or result in every optional piece of SQLite functionality excluded. Almost no one would find it useful, hence why options have to be supplied by the command line and/or setup.cfg.

Good news

The monkey patching I did as rogerbinns/apsw@ee7a632 works perfectly. If a setup.apsw exists when my project setup.py is run then it gets appended to the end of the found config files, and all the options from all the files end up applying to all the commands in the appropriate way.

The project setup.py is not loaded when pip install other packages asks distutils for a dummy install object to get paths that makes a dummy build object that loads setup.cfg.

@joerick joerick closed this as completed May 9, 2023
@joerick
Copy link
Contributor

joerick commented May 9, 2023

Well, glad you got something working!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants