Compare commits

..
221 Commits
Author SHA1 Message Date
Henry Schreiner ffd835cef1 Bump version: v3.1.0 2025-07-23 23:23:06 -04:00
Henry Schreiner 3e2a9aa6e8 fix: regenerate schema
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-07-23 22:47:02 -04:00
cibuildwheel-bot[bot] 10c727eed9 feat: Python 3.14rc1 build by default (#2507) 2025-07-23 20:59:02 -04:00
pre-commit-ci[bot]pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>Henry Schreiner
f628c9dd23 [pre-commit.ci] pre-commit autoupdate (#2505)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.3 → v0.12.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.3...v0.12.4)
- [github.com/pre-commit/mirrors-mypy: v1.16.1 → v1.17.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.1...v1.17.0)

* chore: fix typing issue from match statement

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-07-23 17:18:48 -04:00
0f487ee2cb feat: add support for building Android wheels (#2349)
* Add Android to resource files

* Add Android to miscellaneous places

* Add Android documentation

* Docs cleanups

* Add Android platform module; implement top-level structure and target Python installation

* Implement setup_env and build_wheel

* lru-dict build working

* Alter prefix in sysconfigdata file; fix various issues with FLAGS variables

* Implement Android testing

* Add type annotations to _cross_venv

* Revert Python 3.8 to pip 25.0.1

* Make test-sources required on Android

* Add Android integration tests

* Test cleanups

* Add test of all available Python versions

* Update test-sources and test-command behavior to match iOS

* Documentation cleanups

* Replace Builder class with a set of global functions

* Rename "env" to "build_env"

* Remove Chaquopy repository from default pip command line

* Move native_platform to platforms module

* Fix parse_config_settings

Co-authored-by: Joe Rickerby <joerick@mac.com>

* Add unit tests for parse_config_settings and arch_synonym

* Make `shell_prepared` arguments keyword-only, and add tests for the commands that use it

* Replace `importlib.util.spec_from_file_location` with `runpy.run_path`

* Use python-build-standalone

* Update Android Python

* Enable KVM in Linux CI

* Move KVM code to test_android.py

* Use Java 17 on Azure

* Install emulator if necessary before running -accel-check

* Free up additional disk space on Linux runners

* Add sudo

* Skip emulator tests on CI platforms that don't support it

* Download Android Python from Maven Central

* Free up more disk space on Linux runners

* fix: minor fixups

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Set sysconfig._BASE_PREFIX to support sysconfig.get_path("include")

* Get ANDROID_API_LEVEL from the build environment, not cibuildwheel's own environment

* Correct relative path of test-sources

* Pass a CMake toolchain file to the build

* Add "repair" step which adds libc++ to the wheel when necessary

* Add missing needs_emulator decorator

* Provide useful error message if ANDROID_HOME is not set

* Remove use of HOST environment variable

* Update to Python 3.15.5

* Fix PyLint warnings, clarify comment

* Group common arguments into a dataclass

* Handle environment variables containing newlines

* Discourage the use of `pytest` test commands without `python -m`

* Use single quotes in user-visible messages

* Improve testing documentation

* Pass wheel filename to `log.build_end`

* In GitHub Actions example, skip Android tests on macOS

* Correct relative paths in `patchelf --set-rpath`

* Clarify `test-sources` docs

* Update to Python 3.13.5+20250722.214220

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-07-23 17:18:33 -04:00
Matthieu Darbois e2e24882d8 feat: add default riscv64 images (#2506) 2025-07-23 13:28:36 +02:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> a8bff94dbb [Bot] Update dependencies (#2504)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-07-21 14:16:55 +02:00
Henry Schreiner fbf37256a5 feat: include free-threaded 3.14 by default (#2503)
This is no longer experimental in 3.13 beta 3, so we don't need an enable for it anymore. Someone can skip expliclity with `'cp31?t-*'` if they really don't want free-threaded wheels.

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-07-19 19:38:05 -04:00
Matthieu Darbois 9613645abc feat: add support for manylinux_2_34 i686 image (#2500) 2025-07-18 11:30:42 -07:00
Henry Schreiner 7247e3887c fix: show options for --only (#2499)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-07-17 00:00:49 +02:00
Henry SchreinerandJoe Rickerby d0e4aded3e feat: add summary to Action (#2469)
* chore: add summary to Action

* refactor: new summary table

* fix: fixup tests and formatting

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: pyodide missing some logging

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: nicer printout, nicer in-place summary

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: use summary for everything

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: support only one output wheel from repair

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Add new Github summary format

* Remove a couple of humanize uses

* fix: filter ANSI codes in summary

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: add sha256

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: nicer wheel/wheels depending on how many are present

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-07-15 20:45:53 -07:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> a2c263d4ab [pre-commit.ci] pre-commit autoupdate (#2497)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.2 → v0.12.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.2...v0.12.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-14 21:34:17 -07:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 576ebccc78 [Bot] Update dependencies (#2496)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-07-14 01:31:16 -07:00
Agriya KhetarpalandHenry Schreiner cd9b48cb74 feat: Pyodide 0.28 release (#2487)
* Bump testing version to 0.28.0

* Bump Pyodide constraints

* Add Pyodide 0.28.0 identifier

* Override to a different, alpha version for testing

* Update docs for Pyodide versions everywhere

* Add comment on using older Pyodide alpha version

* Bump to `"cp313*"` for compatibility

* Drop identifier for Pyodide 0.28.0a3

* Add back Pyodide 0.28.0a3 identifier

* Add helpful comment about pyodide-prerelease option

* Fix test and add a comment about it

* Keep one Pyodide version per Python version

* Fix docs for `pyodide-prerelease`; Python ➡️ Pyodide

* Update utils.py

* Drop CI test step for overridden Pyodide version

Suggested-by: Joe Rickerby <joerick@mac.com>

---------

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2025-07-12 18:38:50 -07:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 5e245905b3 [Bot] Update dependencies (#2488)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-07-09 22:14:56 -07:00
Hanlu 157d81eddb chore: remove redundant word in comment (#2492)
Signed-off-by: liangmulu <liangmulu@outlook.com>
2025-07-09 13:57:38 +01:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 75b1860702 [pre-commit.ci] pre-commit autoupdate (#2491)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.1 → v0.12.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.1...v0.12.2)
- [github.com/python-jsonschema/check-jsonschema: 0.33.1 → 0.33.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.1...0.33.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-08 09:00:18 -04:00
MichaIng 807cd81699 fix: typo in riscv64 CPython 3.9 identifier (#2490)
Signed-off-by: MichaIng <micha@dietpi.com>
2025-07-08 08:59:53 -04:00
Henry SchreinerandJoe Rickerby 834ea40510 chore: pattern matching (#2434)
* chore: use match for stringify_setting

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* chore: more match statements

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-07-08 08:59:20 -04:00
Matthieu DarboisandHenry Schreiner 46ef6044e2 feat: add support for manylinux_2_28 i686 image (#2480)
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2025-07-08 09:32:51 +01:00
Henry Schreiner dafc3545eb fix: support only one output wheel from repair (#2478)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-07-08 09:32:22 +01:00
Joe Rickerby a6ec930f15 Welcome to our new platform maintainers! (#2481)
* Add new platform maintainers to README!

* Add a codeowners file
2025-07-08 09:14:34 +01:00
Joe Rickerby 95d2f3a92f Bump version: v3.0.1 2025-07-05 19:03:42 +01:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 40de3fe510 [pre-commit.ci] pre-commit autoupdate (#2483)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.0 → v0.12.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.0...v0.12.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-01 16:58:23 -04:00
920081014e feat: added Python 3.14 preview for iOS (#2475)
* Added Python 3.14 preview for iOS

* Test all Python versions with iOS

* add cp314-ios to the build selector table

---------

Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-06-30 10:32:25 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 94fe0a212f [Bot] Update dependencies (#2482)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-06-30 10:31:30 -04:00
Henry Schreiner 405ddd5315 fix: pyodide missing some logging (#2477)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-28 09:18:18 +02:00
pre-commit-ci[bot]pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>Henry Schreiner
34b4f1e86e [pre-commit.ci] pre-commit autoupdate (#2474)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.13 → v0.12.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.13...v0.12.0)
- [github.com/pre-commit/mirrors-mypy: v1.16.0 → v1.16.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.0...v1.16.1)
- [github.com/python-jsonschema/check-jsonschema: 0.33.0 → 0.33.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.0...0.33.1)

* chore: address new lints

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-24 09:48:01 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> e69b5532ab [Bot] Update dependencies (#2473)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-06-23 13:08:10 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 3e86452449 [Bot] Update dependencies (#2471)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-06-20 10:11:04 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e73749579b chore(deps): bump actions/attest-build-provenance from 2.3.0 to 2.4.0 in the actions group (#2472)
chore(deps): bump actions/attest-build-provenance in the actions group

Bumps the actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/attest-build-provenance` from 2.3.0 to 2.4.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/db473fddc028af60658334401dc6fa3ffd8669fd...e8998f949152b193b063cb0ec769d69d929409be)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 16:08:18 -04:00
Henry Schreiner 588dee0e0c docs: include Windows ARM in examples (#2468)
docs: include Windows ARM
2025-06-14 09:58:07 -04:00
Henry Schreiner 9c53c4ee46 chore: minor tweaks to generate schema script (#2467)
chore: minor tweeks to generate schema script

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-14 14:58:57 +02:00
Joe Rickerby 5f22145df4 Bump version: v3.0.0 2025-06-11 14:02:58 +01:00
Joe Rickerby a73177515a Docs: mobile layout fix (#2466)
* Remove console warning

* Fix table overflow issue on mobile
2025-06-11 08:57:43 +01:00
Henry Schreiner ff86a64577 docs: add tips for numpy (#2465)
* docs: add tips for numpy

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* docs: update example for numpy 2.3.0

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Update docs/faq.md

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-11 08:57:24 +01:00
Henry Schreiner 6f5e480fec chore: use pip's groups in CI (#2463)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-11 08:56:12 +01:00
Henry Schreiner 3c5ff09888 Bump version: v3.0.0rc3 2025-06-11 00:57:01 -04:00
cibuildwheel-bot[bot] 1b9a56e014 [Bot] Update dependencies (#2455) 2025-06-10 21:06:51 -04:00
Henry Schreiner aa9fe2a24e ci: use uv python for docs (binary b1) (#2462)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-10 18:28:34 -04:00
Henry SchreinerandJoe Rickerby e188d9e260 feat: remove 32-bit linux from auto arch, fix auto32 on linux aarch64 (#2458)
* feat: remove 32-bit linux from auto arch, fix auto32 on linux aarch64

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Apply suggestions from code review

Co-authored-by: Joe Rickerby <joerick@mac.com>

* Update cibuildwheel/architecture.py

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-06-10 17:57:41 -04:00
Henry Schreiner 3fa7bd1e72 ci: fix cirrus and reduce rebuilds (#2460)
* ci: fix Cirrus CI on Windows

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* ci: skip unrelated changes more often

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-10 17:57:25 -04:00
Joe Rickerby c6368be701 Move to the OS-latest image tags on Azure Pipelines (#2461) 2025-06-10 16:48:53 -04:00
Henry Schreiner dcecda6962 chore: handle update of cog to 3.5 (#2459)
chore: handle update of cog

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-10 01:54:45 -04:00
Henry Schreiner adc58e93d1 Revert "ci: update example too"
This reverts commit 487154abbb.
2025-06-09 23:39:56 -04:00
Henry Schreiner 487154abbb ci: update example too 2025-06-09 23:15:34 -04:00
Henry Schreiner 0fe82c5882 chore: order python-standalone releases for stability (#2456)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-09 18:32:32 -04:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 0b6c7d0aff [pre-commit.ci] pre-commit autoupdate (#2457)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.12 → v0.11.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.12...v0.11.13)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-09 19:37:40 +01:00
Henry Schreiner 38dab4b641 docs: warning about pyodide-version (#2450) 2025-06-06 09:57:58 -04:00
Henry Schreiner 2edf578499 Bump version: v3.0.0rc2 2025-06-06 00:05:23 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 70c6aba901 [Bot] Update dependencies (#2449)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-06-06 00:00:58 -04:00
Joe Rickerby 31f536bdf3 Bump version: v3.0.0rc1 2025-06-05 11:39:19 +00:00
cibuildwheel-bot[bot] 7601bfcb45 [Bot] Update dependencies (#2443) 2025-06-04 18:21:09 -04:00
Joe Rickerby b5c33b7a09 Docs fixes (#2446) 2025-06-04 18:20:54 -04:00
Henry Schreiner 4e8d0757df chore: touch up release script (#2442)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-03 17:34:22 -04:00
Henry Schreiner a76b6fc338 Bump version: v3.0.0b5 2025-06-03 02:49:08 -04:00
Henry Schreiner ef60c822df ci: fix gitlab Windows with --allow-downgrade
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-03 02:36:02 -04:00
pre-commit-ci[bot]pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>Henry Schreiner
4aa7d7e9c1 [pre-commit.ci] pre-commit autoupdate (#2440)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.11 → v0.11.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.11...v0.11.12)
- [github.com/pre-commit/mirrors-mypy: v1.15.0 → v1.16.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.15.0...v1.16.0)

* chore: update some things for checks

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* chore: fully use PEP 639 licensing

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-02 23:36:28 -04:00
Russell Keith-Magee 03a2ba0bec docs: correct documentation suggesting iOS builds all platforms by default. (#2441) 2025-06-02 23:36:14 -04:00
Joe RickerbyandHenry Schreiner 4aaa823c8e feat: add pyodide 0.28 and pyodide-prerelease (#2431)
* Add pyodide 0.28.0a3

* Add docs and tests for 0.28
This reverts commit 2e7d716a9ecfbdac159eed798ebf1b0170e6858d.

* fix: add selector for pyodide-prerelease, and add graalpy to schema

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-06-02 23:35:59 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> e6b4e2d355 [Bot] Update dependencies (#2439)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-06-02 11:37:35 -04:00
Henry Schreinerandpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> c90accef51 docs: color output for docs (#2407)
* docs: color output for docs

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: use asdf (python-build)

* docs: try to fix colors

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Update .pre-commit-config.yaml

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-01 01:34:20 -04:00
Joe Rickerby 2346c5f6e0 Fix changelog formatting bug 2025-06-01 06:28:20 +01:00
Henry Schreiner 2125d7117e fix: test-sources should use project dir (#2437)
* fix: test-sources should use project dir

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* docs: fix docstring

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-31 23:36:02 -04:00
Henry SchreinerandJoe Rickerby ea08120e25 feat: support multiple commands on iOS (#2432)
* feat: support multiple commands on iOS

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Move split_command into a util module

* (unrelated) fix test docstring

* Implement short-circuit behaviour on test-command

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-05-30 09:53:20 -04:00
Henry SchreinerandMatthieu Darbois f8168e2a89 docs: found more to convert to toml (#2430)
* docs: found more to convert to toml

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Apply suggestions from code review

Co-authored-by: Matthieu Darbois <mayeut@users.noreply.github.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Matthieu Darbois <mayeut@users.noreply.github.com>
2025-05-29 19:54:00 -04:00
Henry Schreiner 3431adedba fix: use standard schema line (#2433)
This was a workaround for a bug in validate-pyproject, but that bug was fixed some time ago, so we can use the standard line here now.

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-29 19:44:38 -04:00
Joe Rickerby 733573feb3 Debug Cirrus CI testing (#2414)
* Skip Cirrus CI iOS testing

* Remove support tick in readme

* Debugging cirrus windows failure

* Use a separate file to avoid error messages from vsdevcmd getting into the output

* cirrus: TEMP target just the test i care about

* Revert "cirrus: TEMP target just the test i care about"

This reverts commit 7ed4417970930ab9e0e1cb1f17497d406310d6ff.

* Remove debug code

* Fix windows issues with multiline -c Python commands
2025-05-29 10:52:46 -04:00
Henry Schreiner c5a3ced8c8 docs: rerender README
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-29 10:37:55 -04:00
Joe Rickerby 6f2d68febf Add missing changelog entries 2025-05-29 12:25:06 +01:00
Henry Schreiner e8a7a374ee docs: typo b3 -> b4 2025-05-29 00:33:43 -04:00
Henry Schreiner cf078b0954 Bump version: v3.0.0b4 2025-05-29 00:27:20 -04:00
Henry Schreiner b9f1942e8d chore: use cog for quick README updates (#2428)
* chore: use cog for quick README updates

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* refactor: rename scripts

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-28 23:36:16 -04:00
Henry Schreiner 2d6c328457 fix: drop PYTHONSAFEPATH (#2429)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-28 19:43:19 -04:00
cibuildwheel-bot[bot]cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>Henry Schreinermayeut
0e8f4f777d [Bot] Update dependencies (#2426)
* Update dependencies

* tests: ignore tag order

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* tests: use sorted platform tags

Per PEP 425, compressed tag sets should be sorted.
This was fixed in auditwheel 6.4.0

* tests: use sorted platform tags

this shouldn't change that often, use static order to allow override.

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: mayeut <mayeut@users.noreply.github.com>
2025-05-28 19:42:00 -04:00
Joe Rickerby 7e099863cd docs: update options table in README and keep up-to-date with commit hook (#2427)
* Update options table in README and keep up-to-date with commit hook

* Separate multiple options with a newline

* remove the old javascript version
2025-05-28 15:38:53 -04:00
Joe Rickerby c41b47f7c2 Bump version: v3.0.0b3 2025-05-28 20:07:38 +01:00
Joe RickerbyandHenry Schreiner 6379772957 Revert to running tests from from a temp dir when test-sources is unset (#2420)
* Revert to running tests from from a temp dir when test-sources is unset

* Fix placeholders error message, add test for it

* Add back {project} placeholders to CIBW_TEST_COMMAND in tests & docs

* Update test/test_before_test.py

---------

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2025-05-28 16:05:16 +01:00
Joe Rickerby 7298563b25 docs: fix many/musllinux option names and values and other broken links (#2425) 2025-05-28 16:04:55 +01:00
Henry Schreiner db2d59be22 chore: use kw_only (Python 3.10+) on most dataclasses (#2422)
* chore: use kw_only (Python 3.10+) on many dataclasses

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: expose windows file to type checker

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-28 10:41:26 +01:00
Henry Schreiner e18ab66cca fix: pyodide missing wheel placeholder (#2421)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-27 19:32:25 -04:00
Henry Schreiner b8814b8148 chore: fix update script to handle pymanager addition to page (#2424)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-27 19:32:14 -04:00
Joe Rickerbyandmayeut e92147f52a [travis-ci] debugging ci failures on travis (#2387)
* [travis-ci] debugging ci failures on travis

* [travis-ci] Use JSON to encode env vars

* [travis-ci] Use a more broadly compatible before_all command

* Drop CIBW_ENABLE=all on Travis windows
It doesn't fit into the time limit

* [travis-ci] try upgrading certifi to get around SSL errors

* Revert "[travis-ci] try upgrading certifi to get around SSL errors"

This reverts commit 02eee0e1946d2116d1ce23ccd86945ad33d1ae24.

* [travis-ci] upgrade windows certificates

* [travis-ci] re-enable linux builds

* Drop CIBW_ENABLE=all on Travis Linux

It doesn't fit into the time limit

---------

Co-authored-by: mayeut <mayeut@users.noreply.github.com>
2025-05-26 23:44:39 -04:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 3bbc772778 [pre-commit.ci] pre-commit autoupdate (#2417)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.10 → v0.11.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.10...v0.11.11)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-26 23:42:40 -04:00
Russell Keith-Magee 2b4a8535d9 fix: ensure that deep paths exist before copying files. (#2418) 2025-05-26 23:42:21 -04:00
Matthieu Darbois 2d6b40d669 fix: rename gp242 identifier to gp311_242 (#2411) 2025-05-26 15:49:59 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> b1d944060a [Bot] Update dependencies (#2415)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-05-26 09:42:05 -04:00
Henry Schreiner e2b6431822 docs: flip TOML and envvars order (#2389) 2025-05-26 00:35:27 -04:00
Joe Rickerby 44d6ccfade Merge remote-tracking branch 'origin/main' 2025-05-25 13:04:36 +01:00
Joe Rickerby dded469d60 Bump version: v3.0.0b2 2025-05-25 13:02:14 +01:00
Henry Schreinerandpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 1bfec0e7c9 chore: move codespell config to pyproject.toml (#2409)
* chore: move codespell config to pyproject.toml

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-25 13:58:13 +02:00
Henry Schreiner 327f5c327d ci: add labels to autoupdate (#2408)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-22 11:01:58 +01:00
Agriya KhetarpalHood ChathamJoe RickerbyJoe RickerbyHenry Schreinerpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
163864158d feat: Pyodide improvements: version setting, standalone environments (#2002)
* Fix a typo: pyoodide ➡️ pyodide

* Add `pyodide_build_version` attribute

* Add version to xbuildenv log step

* Add version to Emscripten log step

* Use `pyodide-build`'s version for updating constraints

* Bump Pyodide constraints by updating `pyodide-build`

* Add a schema for `pyodide-version`

* Update Pyodide constraints

* Bump `pyodide-build` to new 0.29.0

* Test out another Pyodide identifier

* Update outdated Pyodide constraints

* Add Pyodide version to temp directory name

* Remove Pyodide 0.26.1 from build configurations

* Retrieve + validate + install specific xbuildenvs

* Test wheel builds with Pyodide 0.26.2

* Add correct Pyodide version to identifier temp dir

* Don't pre-call Pyodide xbuildenv search

* Fetch just the stable Pyodide versions

* Refactor search + validation + install into one step

* Move all of it under a lock

* Reorder xbuildenv installation

* Add env and cwd to xbuildenv search call

* Temporarily lower to 0.26.2 target

* Separate out search, validate, install; again

* Run xbuildenv search in `CIBW_CACHE_PATH`

* Remove prior `PYODIDE_ROOT` env vars, copy envs

* Validate doesn't need to depend on searching

* Add file lock when searching xbuildenvs

* Test the original version: 0.26.1

* Update Pyodide constraints

* Update constraints for `pyodide-build` 0.29.0 again

* Bump Pyodide from version 0.26.1 ➡️ version 0.26.4

* Add note on compatibility for macOS + other archs

* Note Pyodide version for Pyodide identifier

* Docs about `CIBW_PYODIDE_VERSION`

* Don't fetch just the stable versions

* Discard a variable that's not used later

* Rename `search_xbuildenv` ➡️ `get_xbuildenv_versions`

* `validate_xbuildenv` ➡️ `validate_xbuildenv_version`

* Replace ordered comment, add newline

* Replace sentence on macOS support

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* Capitalise: "pyodide" ➡️ "Pyodide"

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* "work" ➡️ "may succeed"

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* Add another job to test a custom Pyodide version

* Handle "v"-prefixed + non-prefixed versions

* Convert to a proper toml-able option, and remove some hardcoded versions

This removes the enscripten and pyodide-build version specs from
pyproject.toml - pyodide-build is spec'd in the constraints file, and
the emscripten version can be read from the pyodide-build output.

* Add a schema entry

* Add docs for CIBW_PYODIDE_VERSION

* Rephrase

* Add tests for pyodide-version

* Apply suggestions from code review

* Add python_build_standalone util

* Hook up to python-build-standalone, removing dependency on host python

* Remove python hard-code in action.yml

* Add log step

* Add workaround for https://github.com/pyodide/pyodide-build/issues/143

* Add emscripten pytest test

* Remove unneeded checks

* Fix a pytest invoke for emscripten

* Generate pyodide-build constraints from the pinned pyodide version

* Remove pyodide python-build-standalone workaround

* Fixup paths from newer version of pyodide-build

* Fix/skip some failing tests

* Use `python -m pytest` on pyodide, even on Linux

* Docs fixes

* Don't call the github API at runtime, cache the release assets instead

* Ignore pylint false positive

* Add version auto-updating for pyodide

* Add support for pyodide 3.13.

* Fix tests for multiple pyodide wheels

* Remove workaround for unreleased pyodide-build

* Rename to "test_pyodide"

* Fix pathname confusion

* Remove extra github actions job

* Fix expectation for test_abi_none

* Fix the custom_repair_wheel test to actually have clashing names

* Fix pinned version test

* Document test-command limitation

* Remove pyodide 0.28.0a1 for now

* Update constraints files

* Docs/test fixes post removing pyodide cp313

* Fix ABI test expectation

* Docs improvements

* Improve some comments

* Remove logic duplication

* remove pyodide special casing

* Refactor constraints code to use a utility script, circumventing import issues

* chore: nicer nox env

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: typo in variable name found by copilot

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Apply suggestions from code review

* Some more Pyodide version updates in the docs section

* We haven't released Pyodide v0.27.6 yet

* Back to the Github URL for cross-build-environments

* `pyodide-build`, not `emsdk` for Windows skips

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* Move to a separate `_json_request` function

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* Rename "retries" ➡️ "retry_count"

* Add some type hints

* Copy env vars before `UV_CUSTOM_COMPILE_COMMAND`

* Use `HTTPError.headers.get` instead

* Remove extra end quote

* Change download tests URL to `https://badssl.com/`

Co-Authored-By: Joe Rickerby <1244307+joerick@users.noreply.github.com>

* Download size changes, too

* Use jsdelivr for Github asset mirroring

* Bump to Pyodide v0.27.6

* Fix unit tests

* Move to pyodide v0.27.6 again

* Bump to pyodide-build 0.30.4

* Use new URL for cross-build environments metadata

Co-authored-by: Joe Rickerby <joerick@mac.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <1244307+joerick@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-20 16:59:58 -04:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> c3c260dff7 [pre-commit.ci] pre-commit autoupdate (#2403)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.9 → v0.11.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.9...v0.11.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-20 13:43:19 +01:00
Joe Rickerby fcdbed860e Add TEST_ENVIRONMENT option and set PYTHONSAFEPATH=1 in the test env (#2388)
* Add TEST_ENVIRONMENT option and set PYTHONSAFEPATH=1 in the test env

Closes https://github.com/pypa/cibuildwheel/issues/2358

* Add toml docs examples

* Add TOML examples

* Use double-quotes for windows
2025-05-20 13:41:26 +01:00
Joe Rickerby 9dd7192d89 ci: fix failing tests due to rate-limiting (#2401)
raw.githubusercontent.com URLs are rate-limited now, change URLs
2025-05-19 11:58:28 -04:00
Joe Rickerby bd641d11de Add docs note 2025-05-19 10:02:39 +01:00
Joe Rickerby 90a0ddeff0 Bump version: v3.0.0b1 2025-05-19 09:29:14 +01:00
Joe Rickerby e46e9516d9 Fix release script 2025-05-19 09:06:06 +01:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 1fe8e55c67 [Bot] Update dependencies (#2400)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-05-19 08:52:26 +01:00
Joe RickerbyandMalcolm Smith 898f0a45c1 fix: change to the iOS testing option semantics (#2363)
* Change to the iOS testing option semantics

Don't assume the presence of `python -m` in the test command. Less magic
and allows more option reuse between platforms.

* Update schema

* Add a test for this warning

* Don't try to execute a test-command when it doesn't look like a module

* Update docs/options.md

Co-authored-by: Malcolm Smith <smith@chaquo.com>

* Only allow invalid test command if the first part is 'pytest'

* Responses to code review from @freakboy3742

* Fixes post-merge

---------

Co-authored-by: Malcolm Smith <smith@chaquo.com>
2025-05-18 22:42:23 -04:00
Joe Rickerby 1bc7e904b8 Improve integration test options (#2385)
* Improve integration test options

- Keep the platform and enable options in os.environ, don't duplicate state in utils
- Provide more user-friendly options for setting enable and platform in integration tests

* Ensure that CIBW_ENABLE is set in the test process

* Use get_enable_groups() where necessary
2025-05-16 11:11:16 +01:00
Jinzhe Zeng 166465b56d doc: update Windows ARM for GH Actions (#2396)
It's no more cross compilation and can run tests.
2025-05-16 01:41:19 -04:00
Henry Schreiner c2cffa3d1e feat: adding pypy-eol enable group (#2393) 2025-05-15 09:06:48 -04:00
Henry Schreiner cab1ff4076 fix: support Python 3.14's color CLI (#2394)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-05-14 13:04:07 -04:00
cibuildwheel-bot[bot]cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>Henry Schreinerpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
d193a8eff5 tests: fix ci (#2391)
* Update dependencies

* tests: disable color for Pyodide test

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: nist.gov doesn't work anymore for this test

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: limit click for typer

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-14 10:06:34 -04:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> e7dcb31aa7 [pre-commit.ci] pre-commit autoupdate (#2392)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.8 → v0.11.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.8...v0.11.9)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-13 01:28:55 -04:00
Matthieu Darbois 5622cc6988 Add Python 3.14 preview (#2390)
* feat: add CPython 3.14 beta 1

* fix tests
2025-05-12 12:54:02 -04:00
Joe Rickerby 54a4dccd62 Remove official Appveyor support (#2386) 2025-05-11 11:16:54 +02:00
Henry Schreinerandpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 5b65230f64 fix: virtualenv 20.31 no-wheel (#2382)
* fix: virtualenv 20.31 no-wheel

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: show failed output on failure

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update linux.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update linux.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-08 12:12:14 -04:00
Joe Rickerby 04c9427a20 Test enable groups as specified by PR labels (#2357)
* Allow CIBW_ENABLE to control the wheels built in testing

* Set CIBW_ENABLE using PR labels

* Add docs

* Build everything on the main branch

* Add CIBW_ENABLE=all option

This was mostly for use in the `main` building case, because otherwise it's maybe a bit too easy to forget to update this file when adding an enable group

* Remove dead code

* Make unit tests robust to the value of CIBW_ENABLE

* Fix tests that explicitly choose pypy

* Fix test expectation

* Don't expect impossible wheels in expected_wheels

* Simplify logic in expected_wheels

* CircleCI- run with CIBW_ENABLE=all only on the main branch

* Azure pipelines - run with CIBW_ENABLE=all on main branch

* Update gitlab to run CIBW_ENABLE=all on main

* Set CIBW_ENABLE=all on travis - it only runs on main anyway

* Fix job name error on CircleCI

* Fix tests for graalpy

* Update the test configuration to use the label

* Remove duplication of default value. Make it affect sample build too

* Move the action to after deps are installed

* GraalPy workaround for this assumption

* Make unit test resilient to changing CIBW_ENABLE
2025-05-08 12:27:05 +01:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> edbd234b82 [pre-commit.ci] pre-commit autoupdate (#2381)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.7 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.7...v0.11.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-05 16:23:35 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 143ec07556 [Bot] Update dependencies (#2374)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-05-05 16:23:25 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 048ff03577 chore(deps): bump the actions group with 2 updates (#2379)
Bumps the actions group with 2 updates: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) and [wntrblm/nox](https://github.com/wntrblm/nox).


Updates `actions/attest-build-provenance` from 2.2.3 to 2.3.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/c074443f1aee8d4aeeae555aebba3282517141b2...db473fddc028af60658334401dc6fa3ffd8669fd)

Updates `wntrblm/nox` from 2025.02.09 to 2025.05.01
- [Release notes](https://github.com/wntrblm/nox/releases)
- [Changelog](https://github.com/wntrblm/nox/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wntrblm/nox/compare/2025.02.09...2025.05.01)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: wntrblm/nox
  dependency-version: 2025.05.01
  dependency-type: direct:production
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 16:21:01 +01:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> e91560e8a1 [pre-commit.ci] pre-commit autoupdate (#2376)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.6 → v0.11.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.6...v0.11.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-28 18:25:42 -04:00
Tim Felgentreffpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>Henry Schreiner
020a91baa8 feat: support GraalPy (#1538)
* Properly close files used for testing

* Add support for GraalPy

* Help GraalPy discover build tools on Windows

* Expect manylinux-interpreters ensure graalpy* warning in pip

* Workaround GraalPy bugs on Windows

* Workaround oracle/graalpython#491 also when uv is not available

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update azure-pipelines.yml

* Update azure-pipelines.yml

* refacotor: use pathlib.write_text

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Include GraalPy in docker_warmup and remove workaround for installing it late

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2025-04-28 18:25:17 -04:00
Matthieu DarboisandJoe Rickerby 5f8d06ff55 feat: initial support for riscv64 (#2366)
* feat: initial support for riscv64

* use enable for riscv64

* Apply review suggestion

Co-authored-by: Joe Rickerby <joerick@mac.com>

* update documentation

---------

Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-04-28 12:05:03 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0463d26066 chore(deps): bump astral-sh/setup-uv from 5 to 6 in the actions group (#2375)
Bumps the actions group with 1 update: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv).


Updates `astral-sh/setup-uv` from 5 to 6
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 12:02:52 -04:00
Shantanu 4449d7b5fa chore: type check install_certifi.py (#2365) 2025-04-28 12:02:43 -04:00
Joe Rickerby e9a8781000 Merge branch '2.x' 2025-04-26 11:46:15 +01:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 4e5b8ee8e8 [Bot] Update dependencies (#2368)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-04-23 09:36:36 -04:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 03f1905bb9 [pre-commit.ci] pre-commit autoupdate (#2370)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.5 → v0.11.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.5...v0.11.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-21 22:45:23 +01:00
Henry Schreinerandmayeut 99183d68ac ci: add Windows ARM (#2362)
* ci: add Windows ARM

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* ci: install rust on Windows ARM

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* ci: different action for rust

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* ci: no sync required (avoid dev group)

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: expected wheels on Windows ARM64

* fix: skip python 3.8 on Windows ARM64

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: mayeut <mayeut@users.noreply.github.com>
2025-04-15 14:49:17 -04:00
Matthieu Darbois 95e3a6575f fix(CI): Cirrus CI macos jobs (#2355)
* fix(CI): Cirrus CI macos_arm64_cp38 job

* fix(CI): run on macos-runner:sequoia

* doc: update Cirrus CI minimal example
2025-04-14 17:20:26 -04:00
Matthieu Darbois e9fae06abe chore: rework virtual environments creation for tests (#2356)
As mentioned in https://github.com/pypa/cibuildwheel/pull/2347#discussion_r2029821701

Rework the way test venvs are created to reuse the same mechanisms as build venvs.
2025-04-14 17:20:08 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 1fcca39f6d [Bot] Update dependencies (#2359)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-04-14 17:13:04 -04:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 625240d4ff [pre-commit.ci] pre-commit autoupdate (#2360)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.4 → v0.11.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.4...v0.11.5)
- [github.com/python-jsonschema/check-jsonschema: 0.32.1 → 0.33.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.32.1...0.33.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-14 17:11:26 -04:00
Joe RickerbyandHenry Schreiner 1d34f85c63 General docs improvements for v3.0 (#2280)
* Split the docs into sections

* Split the options page into two pages - so the reference is more clean

* Reorder/Tidy up this 'deliver' page to make auto-deploy more obvious

* Fix up some links

* Merge the platform docs into a single page to clean up the navbar.

* Split the setup page to make a page dedicated to CI setup

* Move general platform-specific information into the platforms page

* Tidy up the platforms page

* Remove some redundancy in the cpp/platforms docs

* Clarify this section

* Move another FAQ entry to the platforms page

* Remove some out-of-date entries from the FAQ

* A few corrections to the options page

* Fix some broken links

* Improve the Docker references

* Update bump version filename

* Improve options TOC information density

* whitespace

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>

* Convert the normal test invocation to `pytest ./tests`, not placeholder

---------

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2025-04-13 08:16:55 +02:00
Dimitri Papadopoulos Orfanos 74a7d8a68e chore: add a selection of PERF rules (#2313) 2025-04-13 01:06:33 -04:00
Henry Schreiner 93314a8d65 refactor: pull out dependency_constraint (#2347)
* refactor: pull out dependency_constraint

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* refactor: address feedback

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* ci: better parallel

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-04-09 09:58:57 -04:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 8e2cc78913 [pre-commit.ci] pre-commit autoupdate (#2352)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.2 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.2...v0.11.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-08 06:31:59 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 08cbc6f8d0 chore(deps): bump actions/create-github-app-token from 1 to 2 in the actions group (#2351)
chore(deps): bump actions/create-github-app-token in the actions group

Bumps the actions group with 1 update: [actions/create-github-app-token](https://github.com/actions/create-github-app-token).


Updates `actions/create-github-app-token` from 1 to 2
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 06:31:44 +02:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 1172caba2b [Bot] Update dependencies (#2350)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-04-08 06:31:23 +02:00
Russell Keith-MageeandJoe Rickerby 830e79e11c feat: add the ability to declare safe tools in a cross-build environment. (#2317)
* Add the ability to declare safe tools in a cross-build environment.

* Add an xfail if cmake isn't available on the test machine.

* Placate linter regarding positional args.

* Rework test to provide more robust confirmation of safe tools.

* Remove a test skip condition that is no longer needed.

Co-authored-by: Joe Rickerby <joerick@mac.com>

* Rename the setting to xbuild-tools.

* Add docs to clarify that xbuild-tools is transitive.

* Raise a warning if xbuild-tools isn't defined.

* Correct a bad copy-paste in the schema generator.

* .. and now fix the indentation.

* Move sentinel handling earlier into the parsing process.

* Remove serialization from tests that won't start a test suite.

---------

Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-04-07 08:20:20 +02:00
Henry SchreinerandMatthieu Darbois 2aaa489371 fix: support wider range of verbosity settings on other build backends (#2339)
* fix: support wider range of verbosity settings on other build backends

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Apply suggestions from code review

Co-authored-by: Matthieu Darbois <mayeut@users.noreply.github.com>

* refactor: address review comments

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Matthieu Darbois <mayeut@users.noreply.github.com>
2025-04-06 18:50:24 +02:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 6c426a3a17 [Bot] Update dependencies (#2346)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-04-01 23:28:17 -04:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 55bd91a92a [pre-commit.ci] pre-commit autoupdate (#2343)
updates:
- [github.com/python-jsonschema/check-jsonschema: 0.31.3 → 0.32.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.3...0.32.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-01 10:25:09 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 279221e0d6 [Bot] Update dependencies (#2341)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-03-31 12:46:45 -04:00
Matthieu Darbois 0dd98e95f1 ci: revert QEMU workaround (#2340) 2025-03-30 09:44:32 +01:00
Russell Keith-Magee 4ef7b3770f ci: correct Github Actions CI instability for iOS. (#2337)
* Correct Github Actions CI instability for iOS.

* Add a dummy serial test so CircleCI passes serial test discovery.
2025-03-28 10:33:51 -04:00
Joe Rickerby 094782a41d Merge branch '2.x' 2025-03-24 21:30:34 +00:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 2bb8b1e73d [pre-commit.ci] pre-commit autoupdate (#2333)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-24 20:58:34 +01:00
Matthieu Darbois e6eff86c98 feat: move default manylinux build to manylinux_2_28 (#2330) 2025-03-24 17:00:44 +00:00
Joe Rickerby 141e702872 feat: stricter selector parsing, refactor to platforms module (#2291)
* feat: stricter selector parsing, refactor to `platforms` module

- Use a different method to build nothing
- Make the check aware of enable groups

* optimise unit tests - a specific platform module API for all configs

Unit test time: 26.2s -> 13.1s

* Remove unnecessary get_platform_module function
2025-03-24 16:58:47 +00:00
cibuildwheel-bot[bot]cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>mayeut
4db404ca50 [Bot] Update dependencies (#2332)
* Update dependencies

* fix: stop producing `cibuildwheel/resources/constraints-python37.txt`

---------

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
Co-authored-by: mayeut <mayeut@users.noreply.github.com>
2025-03-24 11:50:40 -04:00
Matthieu DarboisandJoe Rickerby 059d8ea25a feat: drop EOL manylinux images (#2316)
* drop EOL manylinux images

* chore: use multi-arch manylinux images as a source for image tag

Using multi-arch images directly messes with parallel tests for now.
Using them as a source for image tag ensures that all architecture specific tags are pointing to the same tag for a given manylinux / musllinux policy.

* Apply review suggestion

Co-authored-by: Joe Rickerby <joerick@mac.com>

---------

Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-03-22 12:26:38 -04:00
Matthieu Darbois 3b9c8d4d22 fix: don't seed setuptools/wheel in tests virtual environments. (#2329) 2025-03-22 12:25:58 -04:00
Henry Schreiner 462c99589a fix: always resolve --python argument (#2328)
fix: always resolve the --python argument

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-03-22 07:32:25 +01:00
Hadi AlqattanandHenry Schreiner fccd32f9b2 Doc: add a note about Rustc's minimum MACOSX_DEPLOYMENT_TARGET requirement (#2326)
* doc: add a note about rustc's minimum MACOSX_DEPLOYMENT_TARGET requirement

* Update docs/platforms/macos.md

* Update faq.md

---------

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2025-03-22 07:29:45 +01:00
Henry Schreinerandmayeut be2a707315 feat: use build by default (#2321)
* feat: use build by default

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: test/test_before_build.py:test

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: mayeut <mayeut@users.noreply.github.com>
2025-03-21 09:49:04 -04:00
pre-commit-ci[bot]pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>Henry Schreinermayeut
21888b531f [pre-commit.ci] pre-commit autoupdate (#2320)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.10 → v0.11.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.10...v0.11.0)

* chore: use Path.iterdir

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: Path.iterdir usage

os.listdir returned entry names rather than their full path

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: mayeut <mayeut@users.noreply.github.com>
2025-03-21 12:51:51 +00:00
Joe Rickerby 54a0cd0b22 Merge branch '2.x'
# Conflicts:
#	cibuildwheel/__init__.py
#	cibuildwheel/options.py
#	cibuildwheel/resources/pinned_docker_images.cfg
#	cibuildwheel/resources/virtualenv.toml
#	docs/setup.md
#	examples/github-deploy.yml
#	unit_test/options_test.py
2025-03-21 10:41:39 +00:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 1826f70682 [Bot] Update dependencies (#2319)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-03-17 10:10:58 -04:00
Matthieu Darbois 572f2a72b3 fix: image deprecation warning (#2314) 2025-03-12 08:41:51 +00:00
26e12290b5 feat: add support for building iOS wheels. (#2286)
* Add support for building iOS wheels.

* Replace use of system() in test binary module.

* Restored the 'minimal' approach of the minimal examples.

* Split out platform details into standalone pages, and expand iOS platform details.

* More doc corrections.

* Bump support package to include fix for python/cpython#130292

* Ensure iOS tests are all run on the same xdist worker.

* More iOS documentation tweaks.

* Factor out common xcode version test utility.

* Simplify iOS to a single platform with an expanded interpretation of arch.

* I guess I should update the iOS tests as well...

* Additional safety for missing iOS test output.

* Remove DYLD_LIBRARY_PATH from the iOS environment.

* Make test-sources mandatory for iOS builds.

* Updates and clarifications to documentation.

* Clarify what a slice is.

* Normalize use of underscores in platform name.

* Modify auto target to be matching CPU only.

* Use consistent ordering of platforms in examples.

* Use consistent naming in iOS archiectures.

* Placate the linter.

* Miscellaneous cleanups picked up by @joerick's review.

* Correct the list of expected wheels.

* Correct which 'native' we're actually checking.

* Correct the docs links so they're all relative.

* Correct the identification of free threaded builds.

* Use target instead of host to describe the platform we're building for.

* Rework iOS test to remove issue with log completeness.

* Convert errors to FatalError

Co-authored-by: Matthieu Darbois <mayeut@users.noreply.github.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>

* Removed a repeated check for a valid python.

* Update bin/update_pythons.py to update iOS support packages.

* Document that iOS CI is available on other platforms.

* Restore a comment needed for some platforms.

* Small cleanups identified in code review

Co-authored-by: Joe Rickerby <joerick@mac.com>

* Simplify logic to appease linter.

* Modify dependency constraint handling to use new API.

* Cosmetic change to trigger a CI rebuild.

---------

Co-authored-by: Matthieu Darbois <mayeut@users.noreply.github.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-03-11 16:48:35 -04:00
Matthieu Darbois eefd48e266 chore: only build on push to main for Travis CI (#2295) 2025-03-11 16:44:25 -04:00
Henry SchreinerandJoe Rickerby 915d15cc32 fix: warn on deprecated images being set (#2312)
* fix: warn on deprecated images being set

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: handle repeated warning messages

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Apply suggestions from code review

Co-authored-by: Joe Rickerby <joerick@mac.com>

* fix: try making the warnings once per Options

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-03-11 16:44:06 -04:00
Joe Rickerbyandpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 25f2d3f313 Inline dependency-versions syntax (#2122)
* Write docs for inline dependency-versions

* Change the `inline` keyword to `packages` for better readability

* Implement inline package constraints

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add schema for TOML properties

* Change the parsing of the option to parse filenames as-is

* Add a unit test for table-parsing of the option

* Remove unneeded shlex.quote on the dependency-version test

* Tidy-ups, comments, docs fixes

* Add test for empty packages option value

* Fix empty packages scenario

And, remove some optionals to reduce the problem space

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-10 21:45:54 +00:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> f5e502aa88 [pre-commit.ci] pre-commit autoupdate (#2311)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.9.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.9.10)
- [github.com/python-jsonschema/check-jsonschema: 0.31.2 → 0.31.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.2...0.31.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-10 16:30:50 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 849921caef chore(deps): bump actions/attest-build-provenance from 2.2.2 to 2.2.3 in the actions group (#2310)
chore(deps): bump actions/attest-build-provenance in the actions group

Bumps the actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/attest-build-provenance` from 2.2.2 to 2.2.3
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/bd77c077858b8d561b7a36cbe48ef4cc642ca39d...c074443f1aee8d4aeeae555aebba3282517141b2)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 16:30:36 -04:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 8c51c26bf7 [Bot] Update dependencies (#2309)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-03-10 16:30:18 -04:00
Joe Rickerby c136aefe20 chore: optimise unit tests (#2293)
* optimise unit tests - Cache loading the config TOML

Unit test time: 13.1s -> 10.5s

* optimise unit tests - use the same pyproject validator across the tests

Unit test time: 10.5s -> 5.7s

* optimise unit tests - selector optimisation

Unit test time: 5.7s -> 5.1s

* optimise unit tests - build options computation cache

Unit test time: 5.1s -> 3.2s

* Use functools.cache rather than functools.lru_cache

* Don't overwrite build_options when applying functools.cache
2025-03-10 21:29:07 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bddd438a88 chore(deps): bump actions/attest-build-provenance from 2.2.0 to 2.2.2 in the actions group (#2299)
chore(deps): bump actions/attest-build-provenance in the actions group

Bumps the actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/attest-build-provenance` from 2.2.0 to 2.2.2
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/520d128f165991a6c774bcb264f323e3d70747f4...bd77c077858b8d561b7a36cbe48ef4cc642ca39d)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-06 16:55:05 -05:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> ee94e7fdf8 [Bot] Update dependencies (#2298)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-03-06 16:54:56 -05:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ea26d56b5a [pre-commit.ci] pre-commit autoupdate (#2301)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.7 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.7...v0.9.9)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-06 16:54:30 -05:00
Joe Rickerby efa1bedd1f Merge branch '2.x' 2025-03-01 13:35:51 +00:00
Matthieu Darbois 0b052a43b7 fix(ci): Cirrus CI does not allow armv7l builds on aarch64 (#2296) 2025-03-01 11:51:38 +00:00
Joe Rickerby 9e72cd9b54 feat: Print FatalError messages using Logger (#2292)
* feat: Print FatalError messages using Logger

This makes them appear GHA annotations in CI logs, which makes them easier to spot. I've also added the "cibuildwheel: " prefix to those errors, I think it helps when viewing a log to know which tool is talking to you - it can be difficult to understand with build tools.

* Nicer error message on invalid architecture

* Fix some message formatting and test expectations
2025-03-01 10:47:44 +01:00
Henry Schreiner 708cf1d32b style: remove from future import annotations
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-02-28 16:38:15 -05:00
Henry Schreiner 1b63074d76 chore: remove PopenBytes & TYPE_CHECKING
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-02-28 16:38:15 -05:00
Henry Schreiner 783be9bc2f chore: add pathlib check
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-02-28 16:38:15 -05:00
Henry Schreiner 3e37148923 chore: remove staticmethod to appease buggy pylint check
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-02-28 16:38:15 -05:00
Henry Schreiner 183a7c3f1d chore: some cleanup from ruff --preview checks
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-02-28 16:38:15 -05:00
Henry Schreiner 2f62696400 chore: use StrEnum from Python 3.11
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-02-28 16:38:15 -05:00
Henry Schreiner 63a7c44b7f chore: cleanup for 3.11+
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-02-28 16:38:15 -05:00
Lehman Garrison 88ef8dbd29 fix: avoid races in filesystem ops (#2287)
Use `exist_ok` and `missing_ok` in pathlib operations where we can.
2025-02-26 10:56:35 -05:00
Russell Keith-MageeandJoe Rickerby 4d37797aa6 Correct handling of test_sources on universal macOS builds. (#2284)
* Correct handling of test_sources on universal macOS builds.

* Make the test sources modifications macOS only.

* Don't use unittest discovery - that needs an actual test.

* Add comment explaining test source creation

Co-authored-by: Joe Rickerby <joerick@mac.com>

* Simplify dict usage.

---------

Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-02-26 09:13:09 +00:00
Matthieu Darbois 7e5810c550 feat: drop python 3.6 & 3.7 support (#2282) 2025-02-25 14:23:01 -05:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> e061af0b82 [pre-commit.ci] pre-commit autoupdate (#2288)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.6 → v0.9.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.6...v0.9.7)
- [github.com/python-jsonschema/check-jsonschema: 0.31.1 → 0.31.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.1...0.31.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-25 07:44:56 +01:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 6578d1a4f9 [Bot] Update dependencies (#2285)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-02-25 07:44:10 +01:00
Henry Schreiner b08f1ba966 chore: use nox 2025.2.9 features (#2274)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2025-02-25 07:43:09 +01:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 34315ff651 [Bot] Update dependencies (#2278)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-02-19 15:01:11 -05:00
Matthieu Darbois c93d51ec54 feat: add armv7l in auto_archs when running on aarch64 (#2259)
* feat: add armv7l in auto_archs when running on aarch64

This depends on aarch32 EL0 support and thus is done conditionally.

* ci(travis): move to Ubuntu 22.04 / cp312
2025-02-12 10:54:44 -05:00
Matthieu Darbois c738289583 chore: use pypa/manylinux for manylinux_2_31_armv7l (#2269) 2025-02-12 10:52:48 -05:00
Matthieu Darbois a0f457675d doc: add a warning about x86-64-v2 for manylinux_2_34 (#2270) 2025-02-12 10:52:12 -05:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> a787e5d7c9 [Bot] Update dependencies (#2272)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-02-12 10:51:47 -05:00
Matthieu Darbois 9cca4ce881 feature: add PyPy 3.11 (#2268) 2025-02-12 10:51:33 -05:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 91b09c1c85 [pre-commit.ci] pre-commit autoupdate (#2275)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.4 → v0.9.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.4...v0.9.6)
- [github.com/pre-commit/mirrors-mypy: v1.14.1 → v1.15.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.1...v1.15.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-10 23:24:45 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fad71e8d6d chore(deps): bump wntrblm/nox from 2024.10.09 to 2025.02.09 in the actions group (#2273)
chore(deps): bump wntrblm/nox in the actions group

Bumps the actions group with 1 update: [wntrblm/nox](https://github.com/wntrblm/nox).


Updates `wntrblm/nox` from 2024.10.09 to 2025.02.09
- [Release notes](https://github.com/wntrblm/nox/releases)
- [Changelog](https://github.com/wntrblm/nox/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wntrblm/nox/compare/2024.10.09...2025.02.09)

---
updated-dependencies:
- dependency-name: wntrblm/nox
  dependency-type: direct:production
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:17:31 -05:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 3805787fe7 [Bot] Update dependencies (#2267)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-02-09 10:54:24 +01:00
Maxim Evtush 9bfd0800fe Update LICENSE (#2264) 2025-02-06 10:41:03 +00:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 3c5c62cf8c [pre-commit.ci] pre-commit autoupdate (#2261)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.3 → v0.9.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.3...v0.9.4)
- [github.com/codespell-project/codespell: v2.4.0 → v2.4.1](https://github.com/codespell-project/codespell/compare/v2.4.0...v2.4.1)
- [github.com/python-jsonschema/check-jsonschema: 0.31.0 → 0.31.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.0...0.31.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-05 21:54:56 -05:00
Joe Rickerby 0dcc90f3e4 fix: add missing schema for test-sources (#2258) 2025-02-03 15:10:44 -05:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 36259f4582 [Bot] Update dependencies (#2260)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-02-03 15:09:37 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b656a829c8 chore(deps): bump actions/attest-build-provenance from 2.1.0 to 2.2.0 in the actions group (#2254)
chore(deps): bump actions/attest-build-provenance in the actions group

Bumps the actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/attest-build-provenance` from 2.1.0 to 2.2.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/7668571508540a607bdfd90a87a560489fe372eb...520d128f165991a6c774bcb264f323e3d70747f4)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-29 16:09:01 -05:00
pre-commit-ci[bot]pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>Henry Schreiner
e43288dd77 [pre-commit.ci] pre-commit autoupdate (#2255)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.2 → v0.9.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.2...v0.9.3)
- [github.com/codespell-project/codespell: v2.3.0 → v2.4.0](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.0)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update .pre-commit-config.yaml

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2025-01-28 11:18:32 -05:00
Matthieu Darbois b605f8fec8 chore: refactor cibuildwheel.utils (#2252)
* chore: refactor cibuildwheel.utils

* rework build_frontend extra flags

* ci(fix): use tonistiigi/binfmt:qemu-v8.1.5 image for qemu
2025-01-27 14:35:56 -05:00
cibuildwheel-bot[bot]cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>mayeut
318a963cc3 [Bot] Update dependencies (#2253)
* Update dependencies

* ci(fix): use tonistiigi/binfmt:qemu-v8.1.5 image for qemu

* fix(test): implement retry for test_container_removed

The test is flaky on some platforms. Implement retry rather than just skip.

---------

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
Co-authored-by: mayeut <mayeut@users.noreply.github.com>
2025-01-27 14:35:31 -05:00
Matthieu Darbois 3f123af8a1 feat: Add support for ubuntu-24.04-arm GHA runner (#2135)
* ci(gha): add tests on ubuntu-24.04-arm

* address review comments
2025-01-21 14:49:59 -05:00
Matthieu DarboisandJoe Rickerby d134edc652 chore: drop deprecated options related to CIBW_ENABLE (#2095)
* chore: drop deprecated options

* Add --enable option

* Fix pyodide tests


---------

Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-01-21 12:53:01 -05:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 730b64249f [pre-commit.ci] pre-commit autoupdate (#2136)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.1 → v0.9.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.1...v0.9.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-21 07:48:07 +01:00
Matthieu Darbois 526df2f696 chore: fix deprecations warnings in update-dependencies workflow (#2134) 2025-01-21 07:47:53 +01:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 9c2ab266a6 [Bot] Update dependencies (#2123)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-01-20 11:17:58 +00:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> c65b088000 [pre-commit.ci] pre-commit autoupdate (#2127)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.6 → v0.9.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.6...v0.9.1)
- [github.com/python-jsonschema/check-jsonschema: 0.30.0 → 0.31.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.30.0...0.31.0)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-20 11:17:01 +00:00
Matthieu DarboisandJoe Rickerby a96545b729 feat: use python3.11+ for cibuildwheel driver (#1912)
* chore: use SPEC 0 schedule for cibuildwheel

* remove support for {python} and {pip} in commands

* Remove specific Python versions from the update-dependencies job

The requirements pinning is done by uv now, so we don't need to
run the versions of Python to do the pinning anymore.

---------

Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-01-16 10:45:18 -05:00
hidewrong 1608f7567e chore: remove redundant word (#2128) 2025-01-14 22:18:19 +00:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 25e87d8558 [pre-commit.ci] pre-commit autoupdate (#2124)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.8.6)
- [github.com/pre-commit/mirrors-mypy: v1.14.0 → v1.14.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.0...v1.14.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-10 15:37:43 +00:00
Trim21andJoe Rickerby 66295e07b9 download cpython installer to cache dir on macos (#2108)
* cache cpython install on macos

* Update cibuildwheel/macos.py

Co-authored-by: Joe Rickerby <joerick@mac.com>

---------

Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-01-10 15:28:35 +00:00
Agriya Khetarpal 6a3c9552c5 Bump to Pyodide 0.27 (#2117)
* Bump to Pyodide 0.27

* Update the Pyodide target's constraints
2025-01-10 15:28:09 +00:00
Russell Keith-MageeandJoe Rickerby 0b82613d6f Add a test-sources configuration option. (#2062)
* Add test_sources configuration option.

* Add pyodide implementation.

* Modify default test behavior to run in the project directory.

* Update the test for running in the project directory.

* Apply suggestions from code review

Co-authored-by: Joe Rickerby <joerick@mac.com>

* Add shlex quoting to test-sources.

* Restructure ctypes example to avoid issues running from the project folder.

---------

Co-authored-by: Joe Rickerby <joerick@mac.com>
2025-01-10 15:24:24 +00:00
Matthieu Darbois 4d0d9f6fd0 ci: warm-up docker images (#2099)
* ci: warm-up docker images

* don't cache when running emulation tests

* skip graalpy cache

* add comment about the single worker case

* enforce docker warm-up succeeds in CI

* skip warm-up on architectures where it does not do any good
2025-01-10 15:24:02 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 34dbe0e952 chore(deps): bump the actions group across 1 directory with 2 updates (#2116)
Bumps the actions group with 2 updates in the / directory: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) and [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv).


Updates `actions/attest-build-provenance` from 1.4.4 to 2.1.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/ef244123eb79f2f7a7e75d99086184180e6d0018...7668571508540a607bdfd90a87a560489fe372eb)

Updates `astral-sh/setup-uv` from 4 to 5
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: astral-sh/setup-uv
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-05 14:21:42 +00:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 9358823214 [Bot] Update dependencies (#2105)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2025-01-04 13:30:21 +00:00
pre-commit-ci[bot]andpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 148b87f6fe [pre-commit.ci] pre-commit autoupdate (#2107)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.0 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.0...v0.8.4)
- [github.com/pre-commit/mirrors-mypy: v1.13.0 → v1.14.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.13.0...v1.14.0)
- [github.com/python-jsonschema/check-jsonschema: 0.29.4 → 0.30.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.4...0.30.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-24 12:20:22 +01:00
Matthieu Darbois 9c75ea15c2 feat: add manylinux_2_34 (#2098) 2024-11-28 07:52:46 +01:00
Matthieu Darbois 7e66fea502 ci/doc: move azure macOS build to macOS-13 (#2101)
The macOS-12 environment is deprecated.
2024-11-28 07:52:14 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 38f0901d38 chore(deps): bump astral-sh/setup-uv from 3 to 4 in the actions group (#2100)
Bumps the actions group with 1 update: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv).


Updates `astral-sh/setup-uv` from 3 to 4
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v4)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 21:12:12 +01:00
cibuildwheel-bot[bot]andcibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com> 6df11f0f28 [Bot] Update dependencies (#2096)
Update dependencies

Co-authored-by: cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>
2024-11-25 21:10:24 +01:00
Henry Schreiner 3a0db72c17 docs: fix typo in enable (#2097) 2024-11-25 08:24:51 +01:00
Matthieu Darbois 116bf7490d docs: use non deprecated option for free-threading (#2094) 2024-11-23 21:41:05 -05:00
Matthieu Darbois 648448b46a fix: cibuildwheel.schema.json (#2093) 2024-11-23 21:40:46 -05:00
188 changed files with 11542 additions and 5345 deletions
+34 -29
View File
@@ -1,7 +1,30 @@
version: 2
version: 2.1
commands:
cibw_prepare_environment:
description: "Prepare the environment for testing."
steps:
- run:
name: Prepare the environment.
command: bash .circleci/prepare.sh
cibw_run_tests:
description: "Runs tests, with CIBW_ENABLE=all on the main branch"
steps:
- run:
name: Test
command: |
if [ "${CIRCLE_BRANCH}" == "main" ]; then
echo "INFO: Exporting CIBW_ENABLE=all for main branch test run."
export CIBW_ENABLE=all
else
echo "INFO: CIBW_ENABLE not set for this branch test run."
fi
venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
jobs:
osx-python3.12:
osx-python312:
macos:
xcode: 15.4.0
resource_class: macos.m1.medium.gen1
@@ -9,16 +32,10 @@ jobs:
PYTHON: python3
steps:
- checkout
- cibw_prepare_environment
- cibw_run_tests
- run:
name: Prepare the environment.
command: bash .circleci/prepare.sh
- run:
name: Test.
command: venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
linux-python3.12:
linux-python312:
docker:
- image: cimg/python:3.12
environment:
@@ -29,14 +46,8 @@ jobs:
steps:
- checkout
- setup_remote_docker
- run:
name: Prepare the environment.
command: bash .circleci/prepare.sh
- run:
name: Test.
command: venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
- cibw_prepare_environment
- cibw_run_tests
linux-aarch64:
machine:
@@ -49,19 +60,13 @@ jobs:
PYTEST_ADDOPTS: -k "unit_test or main_tests or test_0_basic or test_docker_images"
steps:
- checkout
- run:
name: Prepare the environment.
command: bash .circleci/prepare.sh
- run:
name: Test.
command: venv/bin/python ./bin/run_tests.py
no_output_timeout: 30m
- cibw_prepare_environment
- cibw_run_tests
workflows:
version: 2
all-tests:
jobs:
- osx-python3.12
- linux-python3.12
- osx-python312
- linux-python312
- linux-aarch64
+2 -2
View File
@@ -10,7 +10,7 @@ fi
$PYTHON --version
$PYTHON -m venv venv
venv/bin/python -m pip install -U pip dependency-groups
venv/bin/python -m dependency_groups test | xargs venv/bin/python -m pip install -e.
venv/bin/python -m pip install -U pip
venv/bin/python -m pip install -e. --group test
venv/bin/python -m pip freeze
venv/bin/python --version
+33 -19
View File
@@ -1,13 +1,12 @@
only_if: changesInclude('.cirrus.yml') || ($CIRRUS_BRANCH == "main" && !changesIncludeOnly('docs/*', '.pre-commit-config.yaml')) || $CIRRUS_BRANCH =~ 'cirrus.*'
only_if: changesInclude('.cirrus.yml') || ($CIRRUS_BRANCH == "main" && !changesIncludeOnly('.github/*', 'bin/*', 'docs/*', '.circleci/*', '.travis.yml', '.pre-commit-config.yaml', '.readthedocs.yml', 'azure-pipelines.yml', 'README.md', 'mkdocs.yml', 'noxfile.py')) || $CIRRUS_BRANCH =~ 'cirrus.*'
run_tests: &RUN_TESTS
install_cibuildwheel_script:
- python -m pip install dependency-groups
- python -m dependency_groups test | xargs python -m pip install -e.
- python -m pip install -U pip
- python -m pip install -e. --group test
run_cibuildwheel_tests_script:
- python ./bin/run_tests.py
linux_x86_task:
timeout_in: 120m
compute_engine_instance:
@@ -16,10 +15,15 @@ linux_x86_task:
platform: linux
cpu: 8
memory: 8G
env:
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- apt install -y python3-venv python-is-python3
- add-apt-repository -y ppa:deadsnakes/ppa
- apt-get update
- apt-get install -y python3.12-venv
- python3.12 -m venv ${VENV_ROOT}
<<: *RUN_TESTS
linux_aarch64_task:
@@ -30,10 +34,15 @@ linux_aarch64_task:
platform: linux
cpu: 4
memory: 4G
env:
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- apt install -y python3-venv python-is-python3
- add-apt-repository -y ppa:deadsnakes/ppa
- apt-get update
- apt-get install -y python3.12-venv
- python3.12 -m venv ${VENV_ROOT}
<<: *RUN_TESTS
windows_x86_task:
@@ -46,30 +55,35 @@ windows_x86_task:
memory: 8G
install_pre_requirements_script:
- choco install -y --no-progress python3 --version 3.10.6
- certutil -generateSSTFromWU roots.sst
- certutil -addstore -f root roots.sst
- del roots.sst
- choco install -y --no-progress python3 --version 3.12.4
- refreshenv
- echo PATH=%PATH% >> "%CIRRUS_ENV%"
- powershell -Command "$cleanPath = $env:PATH -replace ';+$', ''; Add-Content -Path $env:CIRRUS_ENV -Value ('PATH=' + $cleanPath)"
<<: *RUN_TESTS
macos_arm64_task:
macos_instance:
image: ghcr.io/cirruslabs/macos-runner:sonoma
image: ghcr.io/cirruslabs/macos-runner:sequoia
env:
PATH: /opt/homebrew/opt/python@3.10/libexec/bin:$PATH
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
install_pre_requirements_script:
- brew install python@3.10
- brew install python@3.12
- python3.12 -m venv ${VENV_ROOT}
<<: *RUN_TESTS
macos_arm64_cp38_task:
macos_instance:
image: ghcr.io/cirruslabs/macos-runner:sonoma
image: ghcr.io/cirruslabs/macos-runner:sequoia
env:
PATH: /opt/homebrew/opt/python@3.10/libexec/bin:$PATH
PYTEST_ADDOPTS: --run-cp38-universal2 -k 'test_cp38_arm64_testing_universal2_installer or test_arch_auto'
VENV_ROOT: ${HOME}/venv-cibuildwheel
PATH: ${VENV_ROOT}/bin:${PATH}
PYTEST_ADDOPTS: --run-cp38-universal2 -k 'test_cp38_arm64_testing_universal2_installer or test_arch_auto or test_dummy_serial'
install_pre_requirements_script:
- brew install python@3.10
- brew install python@3.12
- python3.12 -m venv ${VENV_ROOT}
- curl -fsSLO https://www.python.org/ftp/python/3.8.10/python-3.8.10-macos11.pkg
- sudo installer -pkg python-3.8.10-macos11.pkg -target /
- rm python-3.8.10-macos11.pkg
+2
View File
@@ -0,0 +1,2 @@
cibuildwheel/platforms/ios.py @freakboy3742
cibuildwheel/platforms/pyodide.py @hoodmane @ryanking13 @agriyakhetarpal
+3 -7
View File
@@ -7,15 +7,11 @@ on:
types:
- published
permissions: {}
jobs:
dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: hynek/build-and-inspect-python-package@v2
@@ -31,13 +27,13 @@ jobs:
attestations: write
steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
with:
name: Packages
path: dist
- name: Generate artifact attestation for sdist and wheel
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-path: "dist/cibuildwheel-*"
+79 -44
View File
@@ -6,31 +6,43 @@ on:
- main
- 2.x
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
paths-ignore:
- 'docs/**'
- .ci*
- bin/*
- docs/**
- examples/azure-pipelines-*
- examples/ci*
- examples/travis-ci-*
- .pre-commit-config.yaml
- .readthedocs.yml
- .travis.yml
- README.md
- azure-pipelines.yml
- mkdocs.yml
- noxfile.py
workflow_dispatch:
# allow manual runs on branches without a PR
permissions: {}
concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Linters (mypy, flake8, etc.)
name: Linters (mypy, ruff, etc.)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
id: python
with:
python-version: "3.x"
- uses: j178/prek-action@v1
- uses: pre-commit/action@v3.0.1
- name: PyLint checks
run: pipx run --python "${{ steps.python.outputs.python-path }}" nox -s pylint -- --output-format=github
@@ -40,43 +52,73 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-15-intel, macos-latest]
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-13, macos-15]
python_version: ['3.13']
include:
- os: ubuntu-latest
python_version: '3.8'
python_version: '3.11'
- os: ubuntu-latest
python_version: '3.14'
timeout-minutes: 180
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
name: Install Python ${{ matrix.python_version }}
with:
python-version: ${{ matrix.python_version }}
allow-prereleases: true
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@v6
# free some space to prevent reaching GHA disk space limits
- name: Clean docker images
- name: Free up disk space
if: runner.os == 'Linux'
run: |
docker system prune -a -f
sudo rm -rf $ANDROID_HOME/ndk/{26,28}.* /opt/hostedtoolcache/CodeQL \
/usr/local/lib/node_modules /usr/local/share/chromium \
/usr/local/share/powershell
df -h
# https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/
- name: Enable KVM for Android emulator
if: runner.os == 'Linux' && runner.arch == 'X64'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# for oci_container unit tests
- name: Set up QEMU
if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Install dependencies
run: |
uv sync --no-dev --group test
- uses: joerick/pr-labels-action@v1.0.9
- name: Set CIBW_ENABLE
shell: bash
run: |
if [[ "${{ github.ref_name }}" == "main" ]]; then
CIBW_ENABLE=all
else
# get the default CIBW_ENABLE value from the test module
CIBW_ENABLE=$(uv run --no-sync python -c 'import sys, test.utils as c; sys.stdout.write(c.DEFAULT_CIBW_ENABLE)')
# if this is a PR, check for labels
if [[ -n "$GITHUB_PR_LABEL_CI_PYPY" ]]; then
CIBW_ENABLE+=" pypy"
fi
if [[ -n "$GITHUB_PR_LABEL_CI_GRAALPY" ]]; then
CIBW_ENABLE+=" graalpy"
fi
fi
echo "CIBW_ENABLE=${CIBW_ENABLE}" >> $GITHUB_ENV
- name: Generate a sample project
run: |
uv run -m test.test_projects test.test_0_basic.basic_project sample_proj
uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj
- name: Run a sample build (GitHub Action)
uses: ./
@@ -86,8 +128,6 @@ jobs:
env:
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
CIBW_BUILD_FRONTEND: 'build[uv]'
CIBW_FREE_THREADED_SUPPORT: 1
CIBW_PRERELEASE_PYTHONS: 1
- name: Run a sample build (GitHub Action, only)
uses: ./
@@ -122,14 +162,14 @@ jobs:
test $(find wheelhouse_only -name '*.whl' | wc -l) -eq 1
test $(find wheelhouse_config_file -name '*.whl' | wc -l) -eq 1
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: wheelhouse/*.whl
- name: Test cibuildwheel
run: |
uv run bin/run_tests.py ${{ (runner.os == 'Linux' && runner.arch == 'X64') && '--run-podman' || '' }}
uv run --no-sync bin/run_tests.py ${{ (runner.os == 'Linux' && runner.arch == 'X64') && '--run-podman' || '' }}
emulated-archs:
name: Get qemu emulated architectures
@@ -138,13 +178,11 @@ jobs:
outputs:
archs: ${{ steps.archs.outputs.archs }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@v6
- name: Install dependencies
run: uv sync --no-dev --group test
- name: Get qemu emulated architectures
@@ -163,43 +201,39 @@ jobs:
matrix:
arch: ${{ fromJSON(needs.emulated-archs.outputs.archs) }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@v6
- name: Install dependencies
run: uv sync --no-dev --group test
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Run the emulation tests
run: uv run --no-sync pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py
test-pyodide:
name: Test cibuildwheel building Pyodide wheels
name: Test pyodide
needs: lint
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
name: Install Python 3.12
with:
python-version: '3.12'
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@v6
- name: Install dependencies
run: uv sync --no-dev --group test
- name: Generate a sample project
run: |
uv run -m test.test_projects test.test_0_basic.basic_project sample_proj
uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj
- name: Run a sample build (GitHub Action)
uses: ./
@@ -210,6 +244,7 @@ jobs:
CIBW_PLATFORM: pyodide
- name: Run tests with 'CIBW_PLATFORM' set to 'pyodide'
run: uv run --no-sync ./bin/run_tests.py
run: |
uv run --no-sync ./bin/run_tests.py
env:
CIBW_PLATFORM: pyodide
+61
View File
@@ -0,0 +1,61 @@
name: Update dependencies
on:
pull_request:
paths:
- '.github/workflows/update-dependencies.yml'
- 'bin/update_pythons.py'
- 'bin/update_docker.py'
- 'bin/update_virtualenv.py'
- 'bin/projects.py'
- 'docs/data/projects.yml'
- 'noxfile.py'
workflow_dispatch:
schedule:
- cron: '0 6 * * 1' # "At 06:00 on Monday."
jobs:
update-dependencies:
name: Update dependencies
if: github.repository_owner == 'pypa' || github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
# we use this step to grab a Github App auth token, so that PRs generated by this workflow
# run the GHA tests.
- uses: actions/create-github-app-token@v2
id: generate-token
if: github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel'
with:
app-id: ${{ secrets.CIBUILDWHEEL_BOT_APP_ID }}
private-key: ${{ secrets.CIBUILDWHEEL_BOT_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
- uses: wntrblm/nox@2025.05.01
- name: "Run update: dependencies"
run: nox --force-color -s update_constraints
- name: "Run update: python configs"
run: nox --force-color -s update_pins
- name: "Run update: docs user projects"
run: nox --force-color -s update_proj -- --auth=${{ secrets.GITHUB_TOKEN }}
- name: Create Pull Request
if: github.ref == 'refs/heads/main' && github.repository == 'pypa/cibuildwheel'
uses: peter-evans/create-pull-request@v7
with:
commit-message: Update dependencies
title: '[Bot] Update dependencies'
body: |
Update the versions of our dependencies.
PR generated by "Update dependencies" [workflow](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}).
branch: update-dependencies-pr
sign-commits: true
token: ${{ steps.generate-token.outputs.token }}
delete-branch: true
labels: |
CI: GraalPy
CI: PyPy
dependencies
+1 -3
View File
@@ -21,9 +21,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Update the ${{ env.TAG_NAME }} tag
id: update-major-minor-tag
+1 -6
View File
@@ -84,9 +84,7 @@ celerybeat-schedule
# virtualenv
.venv
venv/
venv3/
venv2/
venv*/
ENV/
env/
env2/
@@ -112,8 +110,5 @@ all_known_setup.yaml
# mkdocs
site/
# Virtual environments
venv*
# PyCharm
.idea/
+20 -8
View File
@@ -13,11 +13,15 @@ linux:
# skip all but the basic tests
# (comment the below line in a PR to debug a Gitlab-specific issue)
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
CIBW_ENABLE: "all"
script:
- curl -sSL https://get.docker.com/ | sh
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- python -m pip install dependency-groups
- python -m dependency_groups test | xargs python -m pip install -e. pytest-custom-exit-code
- python -m pip install -U pip
- python -m pip install -e. pytest-custom-exit-code --group test
- python ./bin/run_tests.py
windows:
@@ -25,21 +29,29 @@ windows:
variables:
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
before_script:
- choco install python -y --version 3.12.4
- choco install python -y --allow-downgrade --version 3.12.4
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
CIBW_ENABLE: "all"
script:
- py -m pip install dependency-groups
- py -m pip install -e. pytest-custom-exit-code $(py -m dependency_groups test)
- py -m pip install -U pip
- py -m pip install -e. pytest-custom-exit-code --group test
- py bin\run_tests.py
tags:
- saas-windows-medium-amd64
macos:
image: macos-latest-xcode-15
image: macos-14-xcode-15
variables:
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
CIBW_ENABLE: "all"
script:
- python3 -m pip install dependency-groups
- python3 -m dependency_groups test | xargs python3 -m pip install -e. pytest-custom-exit-code
- python3 -m pip install -U pip
- python3 -m pip install -e. pytest-custom-exit-code --group test
- python3 ./bin/run_tests.py
tags:
- saas-macos-medium-m1
+24 -21
View File
@@ -14,29 +14,31 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
rev: v0.12.4
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
rev: v1.17.0
hooks:
- id: mypy
name: mypy 3.8 on cibuildwheel/
exclude: ^cibuildwheel/resources/.*py|bin/generate_schema.py$
args: ["--python-version=3.8"]
name: mypy 3.11 on cibuildwheel/
args: ["--python-version=3.11"]
exclude: ^cibuildwheel/resources/_cross_venv.py$ # Requires Python 3.13 or later
additional_dependencies: &mypy-dependencies
- bracex
- build
- dependency-groups>=1.2
- nox
- humanize
- nox>=2025.2.9
- orjson
- packaging
- pyelftools
- pygithub
- pytest<9
- pytest
- rich
- tomli
- tomli_w
- types-certifi
- types-click
@@ -47,9 +49,8 @@ repos:
- uv
- validate-pyproject
- id: mypy
name: mypy 3.12
exclude: ^cibuildwheel/resources/.*py$
args: ["--python-version=3.12"]
name: mypy 3.13
args: ["--python-version=3.13"]
additional_dependencies: *mypy-dependencies
- repo: https://github.com/shellcheck-py/shellcheck-py
@@ -63,31 +64,33 @@ repos:
name: Disallow improper capitalization
language: pygrep
entry: PyBind|Numpy|Cmake|Github|PyTest
types:
- markdown
types: [markdown]
exclude: ^docs/working-examples\.md$ # Autogenerated
- id: update-readme-changelog
name: Update README changelog
- id: cog
name: Cog the README
language: python
entry: bin/update_readme_changelog.py
files: ^docs/changelog.md$
pass_filenames: false
entry: cog -c -P -r -I ./bin README.md
files: '^(README\.md|docs/changelog\.md|docs/options\.md|bin/readme.*)$'
additional_dependencies: [cogapp>=3.5]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
rev: v2.4.1
hooks:
- id: codespell
args: ["-L", "sur,assertin", "-w"]
args: ["-w"]
exclude: ^docs/working-examples\.md$ # Autogenerated
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.4
rev: 0.33.2
hooks:
- id: check-dependabot
- id: check-github-actions
- id: check-github-workflows
- id: check-gitlab-ci
- id: check-readthedocs
# Disabled due to schema issue for now:
# - id: check-readthedocs
- id: check-travis
- id: check-jsonschema
name: Check projects
+1 -3
View File
@@ -4,10 +4,8 @@ version: 2
build:
os: ubuntu-24.04
tools:
python: "3.12"
commands:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
- NO_COLOR=1 uv run --no-dev --group docs mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html
- NO_COLOR=1 uv run --python 3.14 --managed-python --no-dev --group docs mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html
+35 -28
View File
@@ -1,36 +1,34 @@
os: linux
dist: focal
dist: jammy
language: python
branches:
only:
- main
# only build the main branch or PR that explicitely want to test with Travis CI
if: (type = "push") OR (commit_message =~ /travis-ci/)
jobs:
include:
- name: Linux | x86_64 + i686 | Python 3.9
python: 3.9
- name: Linux | x86_64 + i686 | Python 3.12
python: 3.12
services: docker
env: PYTHON=python
env:
- PYTHON=python
# a build using CIBW_ENABLE=all does not fit into Travis' time limit,
# so only the defaults are tested
- name: Linux | arm64 | Python 3.9
python: 3.9
- name: Linux | arm64 | Python 3.12
python: 3.12
services: docker
arch: arm64-graviton2
group: edge
virt: vm
env: PYTHON=python
# docker is outdated in the arm64-graviton2 vm focal image (19.x)
# we need to upgrade to get >= 24.0
addons:
apt:
sources:
- sourceline: 'deb https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable'
packages:
- docker-ce docker-ce-cli containerd.io
arch: arm64
env:
- PYTHON=python
- CIBW_ENABLE=all
- name: Linux | ppc64le | Python 3.9
python: 3.9
- name: Linux | ppc64le | Python 3.12
python: 3.12
services: docker
arch: ppc64le
allow_failure: True
@@ -39,26 +37,35 @@ jobs:
# skip test_manylinuxXXXX_only, it uses too much disk space
# c.f. https://travis-ci.community/t/running-out-of-disk-space-quota-when-using-docker-on-ppc64le/11634
- PYTEST_ADDOPTS='-k "not test_manylinuxXXXX_only"'
- CIBW_ENABLE=all
- name: Windows | x86_64 | Python 3.9
- name: Windows | x86_64 | Python 3.12
os: windows
language: shell
before_install:
- choco upgrade python3 -y --version 3.9.13 --limit-output --params "/InstallDir:C:\\Python39"
# http://woshub.com/updating-trusted-root-certificates-in-windows-10
- certutil -generateSSTFromWU roots.sst
- powershell -Command 'Get-ChildItem -Path roots.sst | Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root'
- rm -f roots.sst
- choco upgrade python3 -y --version 3.12.8 --limit-output --params "/InstallDir:C:\\Python312"
env:
- PYTHON=C:\\Python39\\python
- PYTHON=C:\\Python312\\python
# a build using CIBW_ENABLE=all does not fit into Travis' time limit,
# so only the defaults are tested
- name: Linux | s390x | Python 3.9
python: 3.9
- name: Linux | s390x | Python 3.12
python: 3.12
services: docker
arch: s390x
allow_failure: True
env: PYTHON=python
env:
- PYTHON=python
- CIBW_ENABLE=all
install:
- if [ "${TRAVIS_OS_NAME}" == "linux" ]; then docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all; fi
- $PYTHON -m pip install -U pip dependency-groups
- $PYTHON -m dependency_groups test | xargs $PYTHON -m pip install -e.
- $PYTHON -m pip install -U pip
- $PYTHON -m pip install -e. --group test
script: |
# travis_wait disable the output while waiting
+6 -6
View File
@@ -1,11 +1,11 @@
This is a summary of the host Python versions and platforms covered by the different CI platforms:
| | 3.8 | 3.9 | 3.10 | 3.11 | 3.12 |
|---------|----------------------------------|-----------|-----------|---------|--------------------------------------------------|
| Linux | Azure Pipelines / GitHub Actions | Travis CI | Cirrus CI | | AppVeyor¹ / CircleCI¹ / GitHub Actions / GitLab¹ |
| macOS | Azure Pipelines | | Cirrus CI | GitLab¹ | AppVeyor¹ /CircleCI¹ / GitHub Actions |
| Windows | Azure Pipelines | Travis CI | Cirrus CI | | AppVeyor¹ / GitHub Actions / GitLab¹ |
| | 3.11 | 3.12 | 3.13 |
|---------|----------------------------------|---------------------------------------------|----------------|
| Linux | Azure Pipelines / GitHub Actions | CircleCI¹ / Cirrus CI / GitLab¹ / Travis CI | GitHub Actions |
| macOS | Azure Pipelines | CircleCI¹ / Cirrus CI / GitLab¹ | GitHub Actions |
| Windows | Azure Pipelines | Cirrus CI / GitLab¹ / Travis CI | GitHub Actions |
> ¹ Runs a reduced set of tests to reduce CI load
Non-x86 architectures are covered on Travis CI using Python 3.9.
Non-x86 architectures are covered on Travis CI using Python 3.12.
+1 -1
View File
@@ -1,6 +1,6 @@
This project is licensed under the 'BSD 2-clause license'.
Copyright (c) 2017-2023, Joe Rickerby and contributors. All rights reserved.
Copyright (c) 2017-2025, Joe Rickerby and contributors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
+149 -85
View File
@@ -5,7 +5,6 @@ cibuildwheel
[![Documentation Status](https://readthedocs.org/projects/cibuildwheel/badge/?version=stable)](https://cibuildwheel.pypa.io/en/stable/?badge=stable)
[![Actions Status](https://github.com/pypa/cibuildwheel/workflows/Test/badge.svg)](https://github.com/pypa/cibuildwheel/actions)
[![Travis Status](https://img.shields.io/travis/com/pypa/cibuildwheel/main?logo=travis)](https://travis-ci.com/github/pypa/cibuildwheel)
[![Appveyor status](https://ci.appveyor.com/api/projects/status/gt3vwl88yt0y3hur/branch/main?svg=true)](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/main)
[![CircleCI Status](https://img.shields.io/circleci/build/gh/pypa/cibuildwheel/main?logo=circleci)](https://circleci.com/gh/pypa/cibuildwheel)
[![Azure Status](https://dev.azure.com/joerick0429/cibuildwheel/_apis/build/status/pypa.cibuildwheel?branchName=main)](https://dev.azure.com/joerick0429/cibuildwheel/_build/latest?definitionId=4&branchName=main)
@@ -16,7 +15,7 @@ cibuildwheel
Python wheels are great. Building them across **Mac, Linux, Windows**, on **multiple versions of Python**, is not.
`cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, and GitLab CI - and it builds and tests your wheels across all of your platforms.
`cibuildwheel` is here to help. `cibuildwheel` runs on your CI server - currently it supports GitHub Actions, Azure Pipelines, Travis CI, CircleCI, and GitLab CI - and it builds and tests your wheels across all of your platforms.
What does it do?
@@ -24,30 +23,29 @@ What does it do?
While cibuildwheel itself requires a recent Python version to run (we support the last three releases), it can target the following versions to build wheels:
| | macOS Intel | macOS Apple Silicon | Windows 64bit | Windows 32bit | Windows Arm64 | manylinux<br/>musllinux x86_64 | manylinux<br/>musllinux i686 | manylinux<br/>musllinux aarch64 | manylinux<br/>musllinux ppc64le | manylinux<br/>musllinux s390x | manylinux<br/>musllinux armv7l | Pyodide |
|----------------|----|-----|-----|-----|-----|----|-----|----|-----|-----|---|-----|
| CPython 3.6 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A |
| CPython 3.7 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A |
| CPython 3.8 | ✅ | ✅ | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A |
| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A |
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A |
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A |
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅⁴ |
| CPython 3.13³ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A |
| PyPy 3.7 v7.3 | ✅ | N/A | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A |
| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A |
| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A |
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A |
| PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A |
| | macOS Intel | macOS Apple Silicon | Windows 64bit | Windows 32bit | Windows Arm64 | manylinux<br/>musllinux x86_64 | manylinux<br/>musllinux i686 | manylinux<br/>musllinux aarch64 | manylinux<br/>musllinux ppc64le | manylinux<br/>musllinux s390x | manylinux<br/>musllinux armv7l | Android | iOS | Pyodide |
|--------------------|----|-----|----|-----|-----|----|-----|----|-----|-----|---|-----|-----|-----|
| CPython 3.8 | ✅ | | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.9 | ✅ | | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | ✅⁴ |
| CPython 3.1 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅ | ✅ | N/A |
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.9 v7.3 | ✅ | | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| GraalPy 3.11 v24.2 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | N/A | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
<sup>¹ PyPy is only supported for manylinux wheels.</sup><br>
<sup>¹ PyPy & GraalPy are only supported for manylinux wheels.</sup><br>
<sup>² Windows arm64 support is experimental.</sup><br>
<sup>³ Free-threaded mode requires opt-in using [`CIBW_FREE_THREADED_SUPPORT`](https://cibuildwheel.pypa.io/en/stable/options/#free-threaded-support).</sup><br>
<sup>³ Free-threaded mode requires opt-in on 3.13 using [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable).</sup><br>
<sup>⁴ Experimental, not yet supported on PyPI, but can be used directly in web deployment. Use `--platform pyodide` to build.</sup><br>
<sup>⁵ manylinux armv7l support is experimental. As there are no RHEL based image for this architecture, it's using an Ubuntu based image instead.</sup><br>
- Builds manylinux, musllinux, macOS 10.9+ (10.13+ for Python 3.12+), and Windows wheels for CPython and PyPy
- Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, GitLab CI, and Cirrus CI
- Builds manylinux, musllinux, macOS 10.9+ (10.13+ for Python 3.12+), and Windows wheels for CPython, PyPy, and GraalPy
- Works on GitHub Actions, Azure Pipelines, Travis CI, CircleCI, GitLab CI, and Cirrus CI
- Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate)
- Runs your library's tests against the wheel-installed version of your library
@@ -58,19 +56,19 @@ Usage
`cibuildwheel` runs inside a CI service. Supported platforms depend on which service you're using:
| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM |
|-----------------|-------|-------|---------|-----------|-----------|-------------|
| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² |
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² |
| Travis CI | ✅ | | ✅ | ✅ | | |
| AppVeyor | ✅ | ✅ | ✅ | | | ✅² |
| CircleCI | ✅ | ✅ | | ✅ | ✅ | |
| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | |
| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | |
| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM | Android | iOS |
|-----------------|-------|-------|---------|-----------|-----------|-------------|---------|-----|
| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅⁴ | ✅³ |
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | ✅⁴ | ✅³ |
| Travis CI | ✅ | | ✅ | ✅ | | | ✅⁴ | |
| CircleCI | ✅ | ✅ | || ✅ | | ✅ | ✅³ |
| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | | ✅⁴ | ✅³ |
| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅⁴ | |
<sup>¹ [Requires emulation](https://cibuildwheel.pypa.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.</sup><br>
<sup>² [Uses cross-compilation](https://cibuildwheel.pypa.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.</sup>
<sup>² [Uses cross-compilation](https://cibuildwheel.pypa.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.</sup><br>
<sup>³ Requires a macOS runner; runs tests on the simulator for the runner's architecture.</sup><br>
<sup>⁴ Building for Android requires the runner to be Linux x86_64, macOS ARM64 or macOS x86_64. Testing has [additional requirements](https://cibuildwheel.pypa.io/en/stable/platforms/#android).</sup><br>
<!--intro-end-->
Example setup
@@ -78,6 +76,7 @@ Example setup
To build manylinux, musllinux, macOS, and Windows wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`:
<!--generic-github-start-->
```yaml
name: Build
@@ -89,7 +88,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-15-intel, macos-latest]
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-13, macos-latest]
steps:
- uses: actions/checkout@v4
@@ -98,19 +97,21 @@ jobs:
- uses: actions/setup-python@v5
- name: Install cibuildwheel
run: python -m pip install cibuildwheel==2.23.4
run: python -m pip install cibuildwheel==3.1.0
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
# to supply options, put them in 'env', like:
# env:
# CIBW_SOME_OPTION: value
# ...
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl
```
<!--generic-github-end-->
For more information, including PyPI deployment, and the use of other CI services or the dedicated GitHub Action, check out the [documentation](https://cibuildwheel.pypa.io) and the [examples](https://github.com/pypa/cibuildwheel/tree/main/examples).
@@ -123,33 +124,47 @@ The following diagram summarises the steps that cibuildwheel takes on each platf
<sup>Explore an interactive version of this diagram [in the docs](https://cibuildwheel.pypa.io/en/stable/#how-it-works).</sup>
Options
-------
<!--[[[cog from readme_options_table import get_table; print(get_table()) ]]]-->
<!-- This table is auto-generated from docs/options.md by bin/readme_options_table.py -->
| | Option | Description |
|---|--------|-------------|
| **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.pypa.io/en/stable/options/#platform) | Override the auto-detected target platform |
| | [`CIBW_BUILD`](https://cibuildwheel.pypa.io/en/stable/options/#build-skip) <br> [`CIBW_SKIP`](https://cibuildwheel.pypa.io/en/stable/options/#build-skip) | Choose the Python versions to build |
| | [`CIBW_ARCHS`](https://cibuildwheel.pypa.io/en/stable/options/#archs) | Change the architectures built on your machine by default. |
| | [`CIBW_PROJECT_REQUIRES_PYTHON`](https://cibuildwheel.pypa.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project |
| | [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.pypa.io/en/stable/options/#prerelease-pythons) | Enable building with pre-release versions of Python if available |
| **Build customization** | [`CIBW_BUILD_FRONTEND`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) | Set the tool to use to build, either "pip" (default for now) or "build" |
| | [`CIBW_ENVIRONMENT`](https://cibuildwheel.pypa.io/en/stable/options/#environment) | Set environment variables needed during the build |
| | [`CIBW_ENVIRONMENT_PASS_LINUX`](https://cibuildwheel.pypa.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container during the build. |
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.pypa.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
| | [`CIBW_MANYLINUX_*_IMAGE`<br/>`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.pypa.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.pypa.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
| | [`CIBW_TEST_SKIP`](https://cibuildwheel.pypa.io/en/stable/options/#test-skip) | Skip running tests on some builds |
| **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.pypa.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel |
|---|---|---|
| **Build selection** | [`platform`](https://cibuildwheel.pypa.io/en/stable/options/#platform) | Override the auto-detected target platform |
| | [`build`<br>`skip`](https://cibuildwheel.pypa.io/en/stable/options/#build-skip) | Choose the Python versions to build |
| | [`archs`](https://cibuildwheel.pypa.io/en/stable/options/#archs) | Change the architectures built on your machine by default. |
| | [`project-requires-python`](https://cibuildwheel.pypa.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project |
| | [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) | Enable building with extra categories of selectors present. |
| | [`allow-empty`](https://cibuildwheel.pypa.io/en/stable/options/#allow-empty) | Suppress the error code if no wheels match the specified build identifiers |
| **Build customization** | [`build-frontend`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) | Set the tool to use to build, either "build" (default), "build\[uv\]", or "pip" |
| | [`config-settings`](https://cibuildwheel.pypa.io/en/stable/options/#config-settings) | Specify config-settings for the build backend. |
| | [`environment`](https://cibuildwheel.pypa.io/en/stable/options/#environment) | Set environment variables |
| | [`environment-pass`](https://cibuildwheel.pypa.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container. |
| | [`before-all`](https://cibuildwheel.pypa.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
| | [`before-build`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
| | [`xbuild-tools`](https://cibuildwheel.pypa.io/en/stable/options/#xbuild-tools) | Binaries on the path that should be included in an isolated cross-build environment. |
| | [`repair-wheel-command`](https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
| | [`manylinux-*-image`<br>`musllinux-*-image`](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) | Specify manylinux / musllinux container images |
| | [`container-engine`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify the container engine to use when building Linux wheels |
| | [`dependency-versions`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Control the versions of the tools cibuildwheel uses |
| | [`pyodide-version`](https://cibuildwheel.pypa.io/en/stable/options/#pyodide-version) | Specify the Pyodide version to use for `pyodide` platform builds |
| **Testing** | [`test-command`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | The command to test each built wheel |
| | [`before-test`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
| | [`test-sources`](https://cibuildwheel.pypa.io/en/stable/options/#test-sources) | Paths that are copied into the working directory of the tests |
| | [`test-requires`](https://cibuildwheel.pypa.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
| | [`test-extras`](https://cibuildwheel.pypa.io/en/stable/options/#test-extras) | Install your wheel for testing using `extras_require` |
| | [`test-groups`](https://cibuildwheel.pypa.io/en/stable/options/#test-groups) | Specify test dependencies from your project's `dependency-groups` |
| | [`test-skip`](https://cibuildwheel.pypa.io/en/stable/options/#test-skip) | Skip running tests on some builds |
| | [`test-environment`](https://cibuildwheel.pypa.io/en/stable/options/#test-environment) | Set environment variables for the test environment |
| **Debugging** | [`debug-keep-container`](https://cibuildwheel.pypa.io/en/stable/options/#debug-keep-container) | Keep the container after running for debugging. |
| | [`debug-traceback`](https://cibuildwheel.pypa.io/en/stable/options/#debug-traceback) | Print full traceback when errors occur. |
| | [`build-verbosity`](https://cibuildwheel.pypa.io/en/stable/options/#build-verbosity) | Increase/decrease the output of the build |
These options can be specified in a pyproject.toml file, as well; see [configuration](https://cibuildwheel.pypa.io/en/stable/options/#configuration).
<!--[[[end]]] (sum: FxE3nIgFiY) -->
These options can be specified in a pyproject.toml file, or as environment variables, see [configuration docs](https://cibuildwheel.pypa.io/en/latest/configuration/).
Working examples
----------------
@@ -164,27 +179,26 @@ Here are some repos that use cibuildwheel.
|-----------------------------------|----|----|:------|
| [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. |
| [pytorch-fairseq][] | ![github icon][] | ![apple icon][] ![linux icon][] | Facebook AI Research Sequence-to-Sequence Toolkit written in Python. |
| [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The fundamental package for scientific computing with Python. |
| [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an analytical in-process SQL database management system |
| [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The fundamental package for scientific computing with Python. |
| [Tornado][] | ![github icon][] | ![linux icon][] ![apple icon][] ![windows icon][] | Tornado is a Python web framework and asynchronous networking library. Uses stable ABI for a small C extension. |
| [NCNN][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | ncnn is a high-performance neural network inference framework optimized for the mobile platform |
| [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions |
| [Prophet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Tool for producing high quality forecasts for time series data that has multiple seasonality with linear or non-linear growth. |
| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | The compiled version of MyPy using MyPyC. |
| [Prophet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Tool for producing high quality forecasts for time series data that has multiple seasonality with linear or non-linear growth. |
| [Kivy][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Open source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS |
[scikit-learn]: https://github.com/scikit-learn/scikit-learn
[pytorch-fairseq]: https://github.com/facebookresearch/fairseq
[NumPy]: https://github.com/numpy/numpy
[duckdb]: https://github.com/duckdb/duckdb
[NumPy]: https://github.com/numpy/numpy
[Tornado]: https://github.com/tornadoweb/tornado
[NCNN]: https://github.com/Tencent/ncnn
[Matplotlib]: https://github.com/matplotlib/matplotlib
[Prophet]: https://github.com/facebook/prophet
[MyPy]: https://github.com/mypyc/mypy_mypyc-wheels
[Prophet]: https://github.com/facebook/prophet
[Kivy]: https://github.com/kivy/kivy
[appveyor icon]: docs/data/readme_icons/appveyor.svg
[github icon]: docs/data/readme_icons/github.svg
[azurepipelines icon]: docs/data/readme_icons/azurepipelines.svg
[circleci icon]: docs/data/readme_icons/circleci.svg
@@ -211,15 +225,71 @@ This is similar to static linking, so it might have some license implications. C
Changelog
=========
<!-- START bin/update_readme_changelog.py -->
<!-- [[[cog from readme_changelog import mini_changelog; print(mini_changelog()) ]]] -->
<!-- this section was generated by bin/update_readme_changelog.py -- do not edit manually -->
### v3.1.0
### v2.23.4
_23 July 2025_
_16 March 2026_
- 🐛 Fix HTTP 429 errors while downloading virtualenv from GitHub blob URLs uses a https://bootstrap.pypa.io/ URL instead. (#2775)
- 🌟 CPython 3.14 wheels are now built by default - without the `"cpython-prerelease"` `enable` set. It's time to build and upload these wheels to PyPI! This release includes CPython 3.14.0rc1, which is guaranteed to be ABI compatible with the final release. (#2507) Free-threading is no longer experimental in 3.14, so you have to skip it explicitly with `'cp31?t-*'` if you don't support it yet. (#2503)
- 🌟 Adds the ability to [build wheels for Android](https://cibuildwheel.pypa.io/en/stable/platforms/#android)! Set the [`platform` option](https://cibuildwheel.pypa.io/en/stable/options/#platform) to `android` on Linux or macOS to try it out! (#2349)
- 🌟 Adds Pyodide 0.28, which builds 3.13 wheels (#2487)
- ✨ Support for 32-bit `manylinux_2_28` (now a consistent default) and `manylinux_2_34` added (#2500)
- 🛠 Improved summary, will also use markdown summary output on GHA (#2469)
- 🛠 The riscv64 images now have a working default (as they are now part of pypy/manylinux), but are still experimental (and behind an `enable`) since you can't push them to PyPI yet (#2506)
- 🛠 Fixed a typo in the 3.9 MUSL riscv64 identifier (`cp39-musllinux_ricv64` -> `cp39-musllinux_riscv64`) (#2490)
- 🛠 Mistyping `--only` now shows the correct possibilities, and even suggests near matches on Python 3.14+ (#2499)
- 🛠 Only support one output from the repair step on linux like other platforms; auditwheel fixed this over four years ago! (#2478)
- 🛠 We now use pattern matching extensively (#2434)
- 📚 We now have platform maintainers for our special platforms and interpreters! (#2481)
### v3.0.1
_5 July 2025_
- 🛠 Updates CPython 3.14 prerelease to 3.14.0b3 (#2471)
- ✨ Adds a CPython 3.14 prerelease iOS build (only when prerelease builds are [enabled](https://cibuildwheel.pypa.io/en/stable/options/#enable)) (#2475)
### v3.0.0
_11 June 2025_
See @henryiii's [release post](https://iscinumpy.dev/post/cibuildwheel-3-0-0/) for more info on new features!
- 🌟 Adds the ability to [build wheels for iOS](https://cibuildwheel.pypa.io/en/stable/platforms/#ios)! Set the [`platform` option](https://cibuildwheel.pypa.io/en/stable/options/#platform) to `ios` on a Mac with the iOS toolchain to try it out! (#2286, #2363, #2432)
- 🌟 Adds support for the GraalPy interpreter! Enable for your project using the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable). (#1538, #2411, #2414)
- ✨ Adds CPython 3.14 support, under the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable) `cpython-prerelease`. This version of cibuildwheel uses 3.14.0b2. (#2390)
_While CPython is in beta, the ABI can change, so your wheels might not be compatible with the final release. For this reason, we don't recommend distributing wheels until RC1, at which point 3.14 will be available in cibuildwheel without the flag._ (#2390)
- ✨ Adds the [test-sources option](https://cibuildwheel.pypa.io/en/stable/options/#test-sources), which copies files and folders into the temporary working directory we run tests from. (#2062, #2284, #2420, #2437)
This is particularly important for iOS builds, which do not support placeholders in the `test-command`, but can also be useful for other platforms.
- ✨ Adds [`dependency-versions`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) inline syntax (#2122)
- ✨ Improves support for Pyodide builds and adds the experimental [`pyodide-version`](https://cibuildwheel.pypa.io/en/stable/options/#pyodide-version) option, which allows you to specify the version of Pyodide to use for builds. (#2002)
- ✨ Add `pyodide-prerelease` [enable](https://cibuildwheel.pypa.io/en/stable/options/#enable) option, with an early build of 0.28 (Python 3.13). (#2431)
- ✨ Adds the [`test-environment`](https://cibuildwheel.pypa.io/en/stable/options/#test-environment) option, which allows you to set environment variables for the test command. (#2388)
- ✨ Adds the [`xbuild-tools`](https://cibuildwheel.pypa.io/en/stable/options/#xbuild-tools) option, which allows you to specify tools safe for cross-compilation. Currently only used on iOS; will be useful for Android in the future. (#2317)
- 🛠 The default [manylinux image](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) has changed from `manylinux2014` to `manylinux_2_28`. (#2330)
- 🛠 EOL images `manylinux1`, `manylinux2010`, `manylinux_2_24` and `musllinux_1_1` can no longer be specified by their shortname. The full OCI name can still be used for these images, if you wish. (#2316)
- 🛠 Invokes `build` rather than `pip wheel` to build wheels by default. You can control this via the [`build-frontend`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) option. You might notice that you can see your build log output now! (#2321)
- 🛠 Build verbosity settings have been reworked to have consistent meanings between build backends when non-zero. (#2339)
- 🛠 Removed the `CIBW_PRERELEASE_PYTHONS` and `CIBW_FREE_THREADED_SUPPORT` options - these have been folded into the [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) option instead. (#2095)
- 🛠 Build environments no longer have setuptools and wheel preinstalled. (#2329)
- 🛠 Use the standard Schema line for the integrated JSONSchema. (#2433)
- ⚠️ Dropped support for building Python 3.6 and 3.7 wheels. If you need to build wheels for these versions, use cibuildwheel v2.23.3 or earlier. (#2282)
- ⚠️ The minimum Python version required to run cibuildwheel is now Python 3.11. You can still build wheels for Python 3.8 and newer. (#1912)
- ⚠️ 32-bit Linux wheels no longer built by default - the [arch](https://cibuildwheel.pypa.io/en/stable/options/#archs) was removed from `"auto"`. It now requires explicit `"auto32"`. Note that modern manylinux images (like the new default, `manylinux_2_28`) do not have 32-bit versions. (#2458)
- ⚠️ PyPy wheels no longer built by default, due to a change to our options system. To continue building PyPy wheels, you'll now need to set the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable) to `pypy` or `pypy-eol`. (#2095)
- ⚠️ Dropped official support for Appveyor. If it was working for you before, it will probably continue to do so, but we can't be sure, because our CI doesn't run there anymore. (#2386)
- 📚 A reorganisation of the docs, and numerous updates. (#2280)
- 📚 Use Python 3.14 color output in docs CLI output. (#2407)
- 📚 Docs now primarily use the pyproject.toml name of options, rather than the environment variable name. (#2389)
- 📚 README table now matches docs and auto-updates. (#2427, #2428)
### v2.23.3
@@ -234,23 +304,7 @@ _24 March 2025_
- 🐛 Workaround an issue with pyodide builds when running cibuildwheel with a Python that was installed via UV (#2328 via #2331)
- 🛠 Dependency updates, including a manylinux update that fixes an ['undefined symbol' error](https://github.com/pypa/manylinux/issues/1760) in gcc-toolset (#2334)
### v2.23.1
_15 March 2025_
- ⚠️ Added warnings when the shorthand values `manylinux1`, `manylinux2010`, `manylinux_2_24`, and `musllinux_1_1` are used to specify the images in linux builds. The shorthand to these (unmaintainted) images will be removed in v3.0. If you want to keep using these images, explicitly opt-in using the full image URL, which can be found in [this file](https://github.com/pypa/cibuildwheel/blob/v2.23.1/cibuildwheel/resources/pinned_docker_images.cfg). (#2312)
- 🛠 Dependency updates, including a manylinux update which fixes an [issue with rustup](https://github.com/pypa/cibuildwheel/issues/2303). (#2315)
### v2.23.0
_1 March 2025_
- ✨ Adds official support for the new GitHub Actions Arm runners. In fact these worked out-of-the-box, now we include them in our tests and example configs. (#2135 via #2281)
- ✨ Adds support for building PyPy 3.11 wheels (#2268 via #2281)
- 🛠 Adopts the beta pypa/manylinux image for armv7l builds (#2269 via #2281)
- 🛠 Dependency updates, including Pyodide 0.27 (#2117 and #2281)
<!-- END bin/update_readme_changelog.py -->
<!-- [[[end]]] (sum: HkLT9ZhhtV) -->
---
@@ -270,12 +324,22 @@ Everyone interacting with the cibuildwheel project via codebase, issue tracker,
Maintainers
-----------
Core:
- Joe Rickerby [@joerick](https://github.com/joerick)
- Yannick Jadoul [@YannickJadoul](https://github.com/YannickJadoul)
- Matthieu Darbois [@mayeut](https://github.com/mayeut)
- Henry Schreiner [@henryiii](https://github.com/henryiii)
- Grzegorz Bokota [@Czaki](https://github.com/Czaki)
Platform maintainers:
- Russell Keith-Magee [@freakboy3742](https://github.com/freakboy3742) (iOS)
- Agriya Khetarpal [@agriyakhetarpal](https://github.com/agriyakhetarpal) (Pyodide)
- Hood Chatham [@hoodmane](https://github.com/hoodmane) (Pyodide)
- Gyeongjae Choi [@ryanking13](https://github.com/ryanking13) (Pyodide)
- Tim Felgentreff [@timfel](https://github.com/timfel) (GraalPy)
Credits
-------
+1 -2
View File
@@ -24,11 +24,10 @@ branding:
runs:
using: composite
steps:
# Set up the version of Python that supports pyodide
- uses: actions/setup-python@v5
id: python
with:
python-version: "3.12"
python-version: "3.11 - 3.13"
update-environment: false
- id: cibw
-38
View File
@@ -1,38 +0,0 @@
environment:
matrix:
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2204
APPVEYOR_JOB_NAME: "python312-x64-ubuntu"
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
APPVEYOR_JOB_NAME: "python312-x64-vs2022"
- APPVEYOR_BUILD_WORKER_IMAGE: macos-sonoma
APPVEYOR_JOB_NAME: "python312-x64-macos"
stack: python 3.12
build: off
init:
- ps: |
$BRANCH = if ($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH) { $env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH } else { $env:APPVEYOR_REPO_BRANCH }
if (-not ($BRANCH -eq 'main' -or $BRANCH.ToLower().StartsWith('appveyor-'))) {
$env:PYTEST_ADDOPTS = '-k "unit_test or test_0_basic" --suppress-no-test-exit-code'
}
if ($IsLinux) {
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
}
install:
- python -m pip install -U pip
- python -m pip install -e ".[dev]" pytest-custom-exit-code
# the '-u' flag is required so the output is in the correct order.
# See https://github.com/pypa/cibuildwheel/pull/24 for more info.
test_script: python -u ./bin/run_tests.py
branches:
only:
- main
skip_commits:
files:
- docs/*
+51 -16
View File
@@ -1,42 +1,77 @@
pr:
paths:
exclude:
- docs/*
- .github/**
- bin/*
- docs/**
- examples/github-*
- examples/ci*
- examples/travis-ci-*
- .ci*
- .pre-commit-config.yaml
- .readthedocs.yml
- .travis.yml
- README.md
- mkdocs.yml
- noxfile.py
jobs:
- job: linux_38
timeoutInMinutes: 120
- job: linux_311
timeoutInMinutes: 180
pool: {vmImage: 'ubuntu-latest'}
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
versionSpec: '3.11'
- task: JavaToolInstaller@0
inputs:
versionSpec: '17'
jdkArchitectureOption: 'x64'
jdkSourceOption: 'PreInstalled'
- bash: |
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
python -m pip install dependency-groups
python -m dependency_groups test | xargs python -m pip install -e.
python -m pip install -U pip
python -m pip install -e. --group test
if [ "$(Build.SourceBranch)" = "refs/heads/main" ]; then
echo "INFO: Exporting CIBW_ENABLE=all for main branch test run."
export CIBW_ENABLE=all
else
echo "INFO: CIBW_ENABLE not set for this branch ($(Build.SourceBranch))."
fi
python ./bin/run_tests.py
- job: macos_38
pool: {vmImage: 'macos-latest'}
- job: macos_311
pool: {vmImage: 'macOS-latest'}
timeoutInMinutes: 120
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
versionSpec: '3.11'
- bash: |
python -m pip install dependency-groups
python -m dependency_groups test | xargs python -m pip install -e.
python ./bin/run_tests.py --num-processes 2
python -m pip install -U pip
python -m pip install -e. --group test
if [ "$(Build.SourceBranch)" = "refs/heads/main" ]; then
echo "INFO: Exporting CIBW_ENABLE=all for main branch test run."
export CIBW_ENABLE=all
else
echo "INFO: CIBW_ENABLE not set for this branch ($(Build.SourceBranch))."
fi
python ./bin/run_tests.py
- job: windows_38
- job: windows_311
pool: {vmImage: 'windows-latest'}
timeoutInMinutes: 180
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
versionSpec: '3.11'
- bash: |
python -m pip install dependency-groups
python -m dependency_groups test | xargs python -m pip install -e.
python -m pip install -U pip
python -m pip install -e. --group test
if [ "$(Build.SourceBranch)" = "refs/heads/main" ]; then
echo "INFO: Exporting CIBW_ENABLE=all for main branch test run."
export CIBW_ENABLE=all
else
echo "INFO: CIBW_ENABLE not set for this branch ($(Build.SourceBranch))."
fi
python ./bin/run_tests.py
+5 -13
View File
@@ -1,27 +1,20 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["click", "packaging", "tomli; python_version<'3.11'"]
# dependencies = ["click", "packaging"]
# ///
from __future__ import annotations
import glob
import os
import subprocess
import sys
import tomllib
import urllib.parse
from pathlib import Path
import click
from packaging.version import InvalidVersion, Version
if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib
config = [
# file path, version find/replace format
("pyproject.toml", 'version = "{}"'),
@@ -29,7 +22,6 @@ config = [
("cibuildwheel/__init__.py", '__version__ = "{}"'),
("docs/faq.md", "cibuildwheel=={}"),
("docs/faq.md", "cibuildwheel@v{}"),
("docs/setup.md", "cibuildwheel=={}"),
("examples/*", "cibuildwheel=={}"),
("examples/*", "cibuildwheel@v{}"),
]
@@ -90,7 +82,7 @@ def bump_version() -> None:
actions = []
for path_pattern, version_pattern in config:
paths = [Path(p) for p in glob.glob(path_pattern)]
paths = list(Path().glob(path_pattern))
if not paths:
print(f"error: Pattern {path_pattern} didn't match any files")
@@ -137,8 +129,8 @@ def bump_version() -> None:
contents = contents.replace(find, replace)
path.write_text(contents, encoding="utf8")
print("Files updated. If you want to update the changelog as part of this")
print("commit, do that now.")
print("Files updated. If you want to update docs/changelog.md as part of")
print("this commit, do that now.")
print()
while input('Type "done" to continue: ').strip().lower() != "done":
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
import sys
import textwrap
from pathlib import Path
import click
from cibuildwheel.extra import get_pyodide_xbuildenv_info
@click.command()
@click.argument(
"pyodide-version",
type=str,
)
@click.option(
"--output-file",
type=click.Path(),
default=None,
help="Output file to write the constraints to. If not provided, the constraints will be printed to stdout.",
)
def generate_pyodide_constraints(pyodide_version: str, output_file: str | None = None) -> None:
"""
Generate constraints for a specific Pyodide version. The constraints are
generated based on the Pyodide version's xbuildenv info, which is retrieved
from the Pyodide repository.
These constraints should then be 'pinned' using `uv pip compile`.
Example usage:
bin/generate_pyodide_constraints.py 0.27.0
"""
xbuildenv_info = get_pyodide_xbuildenv_info()
try:
pyodide_version_xbuildenv_info = xbuildenv_info["releases"][pyodide_version]
except KeyError as e:
msg = f"Pyodide version {pyodide_version} not found in xbuildenv info. Versions available: {', '.join(xbuildenv_info['releases'].keys())}"
raise click.BadParameter(msg) from e
pyodide_build_min_version = pyodide_version_xbuildenv_info.get("min_pyodide_build_version")
pyodide_build_max_version = pyodide_version_xbuildenv_info.get("max_pyodide_build_version")
pyodide_build_specifier_parts: list[str] = []
if pyodide_build_min_version:
pyodide_build_specifier_parts.append(f">={pyodide_build_min_version}")
if pyodide_build_max_version:
pyodide_build_specifier_parts.append(f"<={pyodide_build_max_version}")
pyodide_build_specifier = ",".join(pyodide_build_specifier_parts)
constraints_txt = textwrap.dedent(f"""
pip
build[virtualenv]
pyodide-build{pyodide_build_specifier}
click<8.2
""")
if output_file is None:
print(constraints_txt)
else:
Path(output_file).write_text(constraints_txt)
print(f"Constraints written to {output_file}", file=sys.stderr)
if __name__ == "__main__":
generate_pyodide_constraints()
+57 -15
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env -S uv run -q
# /// script
# dependencies = ["pyyaml"]
@@ -6,17 +6,22 @@
import argparse
import copy
import functools
import json
import sys
from typing import Any
import yaml
parser = argparse.ArgumentParser()
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
if sys.version_info >= (3, 14):
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
parser = make_parser()
parser.add_argument("--schemastore", action="store_true", help="Generate schema_store version")
args = parser.parse_args()
starter = """
$schema: http://json-schema.org/draft-07/schema
$schema: http://json-schema.org/draft-07/schema#
$id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json
$defs:
inherit:
@@ -28,9 +33,13 @@ $defs:
description: How to inherit the parent's value.
enable:
enum:
- cpython-experimental-riscv64
- cpython-freethreading
- cpython-prerelease
- graalpy
- pyodide-prerelease
- pypy
- pypy-eol
description: A Python version or flavor to enable.
additionalProperties: false
description: cibuildwheel's settings.
@@ -54,7 +63,7 @@ properties:
type: string_array
build-frontend:
default: default
description: Set the tool to use to build, either "pip" (default for now), "build", or "build[uv]"
description: Set the tool to use to build, either "build" (default), "build[uv]", or "pip"
oneOf:
- enum: [pip, build, "build[uv]", default]
- type: string
@@ -104,7 +113,24 @@ properties:
dependency-versions:
default: pinned
description: Specify how cibuildwheel controls the versions of the tools it uses
type: string
oneOf:
- enum: [pinned, latest]
- type: string
description: Path to a file containing dependency versions, or inline package specifications, starting with "packages:"
not:
enum: [pinned, latest]
- type: object
additionalProperties: false
properties:
file:
type: string
- type: object
additionalProperties: false
properties:
packages:
type: array
items:
type: string
enable:
description: Enable or disable certain builds.
oneOf:
@@ -119,11 +145,6 @@ properties:
description: Set environment variables on the host to pass-through to the container
during the build.
type: string_array
free-threaded-support:
type: boolean
default: false
description: The project supports free-threaded builds of Python (PEP703)
deprecated: Use the `enable` option instead.
manylinux-aarch64-image:
type: string
description: Specify alternative manylinux / musllinux container images
@@ -145,6 +166,9 @@ properties:
manylinux-pypy_x86_64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-riscv64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-s390x-image:
type: string
description: Specify alternative manylinux / musllinux container images
@@ -163,15 +187,24 @@ properties:
musllinux-ppc64le-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-riscv64-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-s390x-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-x86_64-image:
type: string
description: Specify alternative manylinux / musllinux container images
repair-wheel-command:
xbuild-tools:
description: Binaries on the path that should be included in an isolated cross-build environment
type: string_array
pyodide-version:
type: string
description: Specify the version of Pyodide to use
repair-wheel-command:
description: Execute a shell command to repair each built wheel.
type: string_array
skip:
description: Choose the Python versions to skip.
type: string_array
@@ -181,6 +214,9 @@ properties:
test-extras:
description: Install your wheel for testing using `extras_require`
type: string_array
test-sources:
description: Test files that are required by the test environment
type: string_array
test-groups:
description: Install extra groups when testing
type: string_array
@@ -190,6 +226,9 @@ properties:
test-skip:
description: Skip running tests on some builds.
type: string_array
test-environment:
description: Set environment variables for the test environment
type: string_table
"""
schema = yaml.safe_load(starter)
@@ -258,6 +297,7 @@ items:
properties:
before-all: {"$ref": "#/$defs/inherit"}
before-build: {"$ref": "#/$defs/inherit"}
xbuild-tools: {"$ref": "#/$defs/inherit"}
before-test: {"$ref": "#/$defs/inherit"}
config-settings: {"$ref": "#/$defs/inherit"}
container-engine: {"$ref": "#/$defs/inherit"}
@@ -266,18 +306,19 @@ items:
repair-wheel-command: {"$ref": "#/$defs/inherit"}
test-command: {"$ref": "#/$defs/inherit"}
test-extras: {"$ref": "#/$defs/inherit"}
test-sources: {"$ref": "#/$defs/inherit"}
test-requires: {"$ref": "#/$defs/inherit"}
test-environment: {"$ref": "#/$defs/inherit"}
"""
)
for key, value in schema["properties"].items():
value["title"] = f'CIBW_{key.replace("-", "_").upper()}'
value["title"] = f"CIBW_{key.replace('-', '_').upper()}"
non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]}
del non_global_options["build"]
del non_global_options["skip"]
del non_global_options["test-skip"]
del non_global_options["free-threaded-support"]
del non_global_options["enable"]
overrides["items"]["properties"]["select"]["oneOf"] = string_array
@@ -307,6 +348,8 @@ oses = {
"windows": as_object(not_linux),
"macos": as_object(not_linux),
"pyodide": as_object(not_linux),
"android": as_object(not_linux),
"ios": as_object(not_linux),
}
oses["linux"]["properties"]["repair-wheel-command"] = {
@@ -325,9 +368,8 @@ schema["properties"] |= oses
if args.schemastore:
schema["$id"] = "https://json.schemastore.org/partial-cibuildwheel.json"
schema["$schema"] = "http://json-schema.org/draft-07/schema#"
schema["description"] = (
"cibuildwheel's toml file, generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
"cibuildwheel's settings. Generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
)
print(json.dumps(schema, indent=2))
-2
View File
@@ -11,8 +11,6 @@ This will cache the results to all_known_setup.yaml; you can reprint
the results without the `--online` setting.
"""
from __future__ import annotations
import ast
from collections.abc import Iterable, Iterator
from pathlib import Path
+1 -2
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import subprocess
@@ -56,7 +55,7 @@ def main() -> None:
f"""
Update the versions of our dependencies.
PR generated by `{os.path.basename(__file__)}`.
PR generated by `{Path(__file__).name}`.
"""
)
subprocess.run(
+6 -8
View File
@@ -17,8 +17,6 @@ Suggested usage:
git diff
"""
from __future__ import annotations
import builtins
import functools
import textwrap
@@ -28,14 +26,13 @@ from collections.abc import Iterable, Mapping, Sequence
from datetime import datetime
from io import StringIO
from pathlib import Path
from typing import Any, TextIO
from typing import Any, Self, TextIO
import click
import yaml
from github import Github, GithubException
ICONS = (
"appveyor",
"github",
"azurepipelines",
"circleci",
@@ -85,7 +82,7 @@ class Project:
name_len = len(self.name) + 4
self.__class__.NAME = max(self.__class__.NAME, name_len)
def __lt__(self, other: Project) -> bool:
def __lt__(self, other: Self) -> bool:
if self.online:
return self.num_stars < other.num_stars
else:
@@ -95,8 +92,8 @@ class Project:
def header(cls) -> str:
return textwrap.dedent(
f"""\
| {'Name':{cls.NAME}} | CI | OS | Notes |
|{'':-^{cls.NAME+2 }}|----|----|:------|"""
| {"Name":{cls.NAME}} | CI | OS | Notes |
|{"":-^{cls.NAME + 2}}|----|----|:------|"""
)
@property
@@ -138,6 +135,7 @@ def fetch_icon(icon_name: str) -> None:
document = xml.dom.minidom.parseString(original_svg_data)
svgElement = document.documentElement
assert svgElement is not None
assert svgElement.nodeName == "svg"
svgElement.setAttribute("width", "16px")
svgElement.setAttribute("fill", "#606060")
@@ -219,7 +217,7 @@ def insert_projects_table(
generated_note = f"<!-- this section is generated by bin/projects.py. Don't edit it directly, instead, edit {input_filename} -->"
new_text = (
f"{text[:start + len(start_str)]}\n{generated_note}\n\n{projects_table}\n{text[end:]}"
f"{text[: start + len(start_str)]}\n{generated_note}\n\n{projects_table}\n{text[end:]}"
)
file.write_text(new_text)
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
import re
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent / ".."
CHANGELOG_FILE = PROJECT_ROOT / "docs" / "changelog.md"
# https://regexr.com/622ds
FIRST_5_CHANGELOG_ENTRIES_REGEX = re.compile(r"""(^###.*?(?=###)){5}""", re.DOTALL | re.MULTILINE)
def mini_changelog() -> str:
changelog_text = CHANGELOG_FILE.read_text()
mini_changelog_match = FIRST_5_CHANGELOG_ENTRIES_REGEX.search(changelog_text)
assert mini_changelog_match, "Failed to find the first few changelog entries"
return f"\n{mini_changelog_match.group(0).strip()}\n"
if __name__ == "__main__":
print(mini_changelog())
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
import dataclasses
import re
from pathlib import Path
from typing import Final
DIR: Final[Path] = Path(__file__).parent.parent.resolve()
OPTIONS_MD: Final[Path] = DIR / "docs" / "options.md"
SECTION_HEADER_REGEX = re.compile(r"^## (?P<name>.*?)$", re.MULTILINE)
# https://regexr.com/8f1ff
OPTION_HEADER_REGEX = re.compile(
r"^### (?P<name>.*?){.*#(?P<id>\S+).*}\n+> ?(?P<desc>.*)$", re.MULTILINE
)
@dataclasses.dataclass(kw_only=True)
class Option:
name: str
id: str
desc: str
section: str
def get_table() -> str:
options_md = OPTIONS_MD.read_text(encoding="utf-8")
sections = SECTION_HEADER_REGEX.split(options_md)[1:]
options = []
for section_name, section_content in zip(sections[0::2], sections[1::2], strict=True):
for match in OPTION_HEADER_REGEX.finditer(section_content):
option = Option(
name=match.group("name").strip(),
id=match.group("id").strip(),
desc=match.group("desc").strip(),
section=section_name.strip(),
)
options.append(option)
table_md = "\n<!-- This table is auto-generated from docs/options.md by bin/readme_options_table.py -->\n\n"
table_md += "| | Option | Description |\n"
table_md += "|---|---|---|\n"
last_section: str | None = None
for option in options:
cells: list[str] = []
cells.append(f"**{option.section}**" if option.section != last_section else "")
last_section = option.section
url = f"https://cibuildwheel.pypa.io/en/stable/options/#{option.id}"
name = option.name.replace(", ", "<br>") # Replace commas with line breaks
cells.append(f"[{name}]({url})")
cells.append(option.desc)
table_md += "| " + " | ".join(cells) + " |\n"
table_md += "\n"
return table_md
if __name__ == "__main__":
print(get_table())
+11 -19
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import shutil
@@ -9,12 +8,13 @@ import sys
import textwrap
import time
import typing
from glob import glob
from pathlib import Path
from urllib.parse import quote
import click
DIR = Path(__file__).parent.resolve()
def shell(cmd: str, *, check: bool, **kwargs: object) -> subprocess.CompletedProcess[str]:
return subprocess.run([cmd], shell=True, check=check, **kwargs) # type: ignore[call-overload, no-any-return]
@@ -28,7 +28,7 @@ def git_repo_has_changes() -> bool:
def generate_basic_project(path: Path) -> None:
sys.path.insert(0, "")
from test.test_projects.c import new_c_project
from test.test_projects.c import new_c_project # noqa: PLC0415
project = new_c_project()
project.generate(path)
@@ -41,11 +41,6 @@ class CIService(typing.NamedTuple):
services = [
CIService(
name="appveyor",
dst_config_path="appveyor.yml",
badge_md="[![Build status](https://ci.appveyor.com/api/projects/status/gt3vwl88yt0y3hur/branch/{branch}?svg=true)](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/{branch})",
),
CIService(
name="azure-pipelines",
dst_config_path="azure-pipelines.yml",
@@ -79,9 +74,8 @@ services = [
]
def ci_service_for_config_file(config_file: str) -> CIService:
filename = Path(config_file).name
def ci_service_for_config_file(config_file: Path) -> CIService:
filename = config_file.name
try:
return next(s for s in services if filename.startswith(s.name))
except StopIteration:
@@ -98,16 +92,16 @@ def run_example_ci_configs(config_files=None):
"""
if len(config_files) == 0:
config_files = glob("examples/*-minimal.yml")
config_files = Path("examples").glob("*-minimal.yml")
# check each CI service has at most 1 config file
configs_by_service = {}
configs_by_service = set()
for config_file in config_files:
service = ci_service_for_config_file(config_file)
if service.name in configs_by_service:
msg = "You cannot specify more than one config per CI service"
raise Exception(msg)
configs_by_service[service.name] = config_file
configs_by_service.add(service.name)
if git_repo_has_changes():
print("Your git repo has uncommitted changes. Commit or stash before continuing.")
@@ -128,18 +122,17 @@ def run_example_ci_configs(config_files=None):
for config_file in config_files:
service = ci_service_for_config_file(config_file)
src_config_file = Path(config_file)
dst_config_file = example_project / service.dst_config_path
dst_config_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(src_config_file, dst_config_file)
shutil.copyfile(config_file, dst_config_file)
subprocess.run(["git", "add", example_project], check=True)
message = textwrap.dedent(
f"""\
Test example minimal configs
Testing files: {config_files}
Testing files: {[str(f) for f in config_files]}
Generated from branch: {previous_branch}
Time: {timestamp}
"""
@@ -174,6 +167,5 @@ def run_example_ci_configs(config_files=None):
if __name__ == "__main__":
os.chdir(os.path.dirname(__file__))
os.chdir("..")
os.chdir(DIR)
run_example_ci_configs(standalone_mode=True)
+42 -4
View File
@@ -1,16 +1,23 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import functools
import os
import subprocess
import sys
from pathlib import Path
if __name__ == "__main__":
default_cpu_count = os.cpu_count() or 2
parser = argparse.ArgumentParser()
if sys.version_info < (3, 13):
default_cpu_count = os.cpu_count() or 2
else:
default_cpu_count = os.process_cpu_count() or 2
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
if sys.version_info >= (3, 14):
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
parser = make_parser()
parser.add_argument(
"--run-podman", action="store_true", default=False, help="run podman tests (linux only)"
)
@@ -35,22 +42,53 @@ if __name__ == "__main__":
if args.run_podman:
unit_test_args += ["--run-podman"]
print(
"\n\n================================== UNIT TESTS ==================================",
flush=True,
)
subprocess.run(unit_test_args, check=True)
# integration tests
# Run the serial integration tests without multiple processes
serial_integration_test_args = [
sys.executable,
"-m",
"pytest",
"-m",
"serial",
"-x",
"--durations",
"0",
"--timeout=2400",
"test",
"-vv",
]
print(
"\n\n=========================== SERIAL INTEGRATION TESTS ===========================",
flush=True,
)
subprocess.run(serial_integration_test_args, check=True)
# Non-serial integration tests
integration_test_args = [
sys.executable,
"-m",
"pytest",
"-m",
"not serial",
f"--numprocesses={args.num_processes}",
"-x",
"--durations",
"0",
"--timeout=2400",
"test",
"-vv",
]
if sys.platform.startswith("linux") and args.run_podman:
integration_test_args += ["--run-podman"]
print(
"\n\n========================= NON-SERIAL INTEGRATION TESTS =========================",
flush=True,
)
subprocess.run(integration_test_args, check=True)
+5 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import functools
import os
import subprocess
import sys
@@ -13,7 +13,10 @@ if __name__ == "__main__":
# move cwd to the project root
os.chdir(Path(__file__).resolve().parents[1])
parser = argparse.ArgumentParser(description="Runs a sample build")
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
if sys.version_info >= (3, 14):
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
parser = make_parser(description="Runs a sample build")
parser.add_argument("project_python_path", nargs="?", default="test.test_0_basic.basic_project")
options = parser.parse_args()
+64 -53
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
from __future__ import annotations
import configparser
from dataclasses import dataclass
import dataclasses
from pathlib import Path
import requests
@@ -12,63 +11,72 @@ DIR = Path(__file__).parent.resolve()
RESOURCES = DIR.parent / "cibuildwheel/resources"
@dataclass(frozen=True)
@dataclasses.dataclass(frozen=True)
class Image:
manylinux_version: str
platform: str
platforms: list[str]
image_name: str
tag: str | None # Set this to pin the image
tag: str | None = None # Set this to pin the image
use_platform_suffix: bool = False
class PyPAImage(Image):
def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None = None):
image_name = f"quay.io/pypa/{manylinux_version}"
super().__init__(manylinux_version, platforms, image_name, tag, True)
images = [
# manylinux1 images
Image("manylinux1", "x86_64", "quay.io/pypa/manylinux1_x86_64", None),
Image("manylinux1", "i686", "quay.io/pypa/manylinux1_i686", None),
# manylinux2010 images
Image("manylinux2010", "x86_64", "quay.io/pypa/manylinux2010_x86_64", None),
Image("manylinux2010", "i686", "quay.io/pypa/manylinux2010_i686", None),
Image("manylinux2010", "pypy_x86_64", "quay.io/pypa/manylinux2010_x86_64", None),
Image("manylinux2010", "pypy_i686", "quay.io/pypa/manylinux2010_i686", None),
# manylinux2014 images
Image("manylinux2014", "x86_64", "quay.io/pypa/manylinux2014_x86_64", None),
Image("manylinux2014", "i686", "quay.io/pypa/manylinux2014_i686", None),
Image("manylinux2014", "aarch64", "quay.io/pypa/manylinux2014_aarch64", None),
Image("manylinux2014", "ppc64le", "quay.io/pypa/manylinux2014_ppc64le", None),
Image("manylinux2014", "s390x", "quay.io/pypa/manylinux2014_s390x", None),
Image("manylinux2014", "pypy_x86_64", "quay.io/pypa/manylinux2014_x86_64", None),
Image("manylinux2014", "pypy_i686", "quay.io/pypa/manylinux2014_i686", None),
Image("manylinux2014", "pypy_aarch64", "quay.io/pypa/manylinux2014_aarch64", None),
# manylinux_2_24 images
Image("manylinux_2_24", "x86_64", "quay.io/pypa/manylinux_2_24_x86_64", None),
Image("manylinux_2_24", "i686", "quay.io/pypa/manylinux_2_24_i686", None),
Image("manylinux_2_24", "aarch64", "quay.io/pypa/manylinux_2_24_aarch64", None),
Image("manylinux_2_24", "ppc64le", "quay.io/pypa/manylinux_2_24_ppc64le", None),
Image("manylinux_2_24", "s390x", "quay.io/pypa/manylinux_2_24_s390x", None),
Image("manylinux_2_24", "pypy_x86_64", "quay.io/pypa/manylinux_2_24_x86_64", None),
Image("manylinux_2_24", "pypy_i686", "quay.io/pypa/manylinux_2_24_i686", None),
Image("manylinux_2_24", "pypy_aarch64", "quay.io/pypa/manylinux_2_24_aarch64", None),
PyPAImage(
"manylinux2014",
[
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
"pypy_x86_64",
"pypy_i686",
"pypy_aarch64",
],
),
# manylinux_2_28 images
Image("manylinux_2_28", "x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
Image("manylinux_2_28", "aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
Image("manylinux_2_28", "ppc64le", "quay.io/pypa/manylinux_2_28_ppc64le", None),
Image("manylinux_2_28", "s390x", "quay.io/pypa/manylinux_2_28_s390x", None),
Image("manylinux_2_28", "pypy_x86_64", "quay.io/pypa/manylinux_2_28_x86_64", None),
Image("manylinux_2_28", "pypy_aarch64", "quay.io/pypa/manylinux_2_28_aarch64", None),
PyPAImage(
"manylinux_2_28",
[
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
"pypy_x86_64",
"pypy_i686",
"pypy_aarch64",
],
),
# manylinux_2_31 images
Image("manylinux_2_31", "armv7l", "quay.io/pypa/manylinux_2_31_armv7l", None),
# musllinux_1_1 images
Image("musllinux_1_1", "x86_64", "quay.io/pypa/musllinux_1_1_x86_64", None),
Image("musllinux_1_1", "i686", "quay.io/pypa/musllinux_1_1_i686", None),
Image("musllinux_1_1", "aarch64", "quay.io/pypa/musllinux_1_1_aarch64", None),
Image("musllinux_1_1", "ppc64le", "quay.io/pypa/musllinux_1_1_ppc64le", None),
Image("musllinux_1_1", "s390x", "quay.io/pypa/musllinux_1_1_s390x", None),
PyPAImage("manylinux_2_31", ["armv7l"]),
# manylinux_2_34 images
PyPAImage(
"manylinux_2_34",
[
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
"pypy_x86_64",
"pypy_i686",
"pypy_aarch64",
],
),
# manylinux_2_39 images
PyPAImage("manylinux_2_39", ["riscv64"]),
# musllinux_1_2 images
Image("musllinux_1_2", "x86_64", "quay.io/pypa/musllinux_1_2_x86_64", None),
Image("musllinux_1_2", "i686", "quay.io/pypa/musllinux_1_2_i686", None),
Image("musllinux_1_2", "aarch64", "quay.io/pypa/musllinux_1_2_aarch64", None),
Image("musllinux_1_2", "ppc64le", "quay.io/pypa/musllinux_1_2_ppc64le", None),
Image("musllinux_1_2", "s390x", "quay.io/pypa/musllinux_1_2_s390x", None),
Image("musllinux_1_2", "armv7l", "quay.io/pypa/musllinux_1_2_armv7l", None),
PyPAImage(
"musllinux_1_2", ["x86_64", "i686", "aarch64", "ppc64le", "s390x", "armv7l", "riscv64"]
),
]
config = configparser.ConfigParser()
@@ -124,10 +132,13 @@ for image in images:
)
tag_name = pinned_tag["name"]
if not config.has_section(image.platform):
config[image.platform] = {}
config[image.platform][image.manylinux_version] = f"{image.image_name}:{tag_name}"
for platform in image.platforms:
if not config.has_section(platform):
config[platform] = {}
suffix = ""
if image.use_platform_suffix:
suffix = f"_{platform.removeprefix('pypy_')}"
config[platform][image.manylinux_version] = f"{image.image_name}{suffix}:{tag_name}"
with open(RESOURCES / "pinned_docker_images.cfg", "w") as f:
config.write(f)
+1 -3
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
from __future__ import annotations
import subprocess
import sys
@@ -42,8 +41,7 @@ def main() -> None:
)
dest_path = Path("docs/data/how-it-works.png")
if dest_path.exists():
dest_path.unlink()
dest_path.unlink(missing_ok=True)
Path(screenshot).rename(dest_path)
+3 -5
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
from __future__ import annotations
import dataclasses
import difflib
import logging
from dataclasses import dataclass
import tomllib
from pathlib import Path
from typing import Final
@@ -16,8 +16,6 @@ from packaging.version import InvalidVersion, Version
from rich.logging import RichHandler
from rich.syntax import Syntax
from cibuildwheel._compat import tomllib
log = logging.getLogger("cibw")
# Looking up the dir instead of using utils.resources_dir
@@ -29,7 +27,7 @@ NODEJS_DIST: Final[str] = "https://nodejs.org/dist/"
NODEJS_INDEX: Final[str] = f"{NODEJS_DIST}index.json"
@dataclass(frozen=True, order=True)
@dataclasses.dataclass(frozen=True, order=True)
class VersionTuple:
version: Version
version_string: str
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
import json
from cibuildwheel.extra import github_api_request
from cibuildwheel.util.python_build_standalone import (
PythonBuildStandaloneAsset,
PythonBuildStandaloneReleaseData,
)
from cibuildwheel.util.resources import PYTHON_BUILD_STANDALONE_RELEASES
def main() -> None:
"""
This script updates the vendored list of release assets to the latest
version of astral-sh/python-build-standalone.
"""
# Get the latest release tag from the GitHub API
latest_release = github_api_request("repos/astral-sh/python-build-standalone/releases/latest")
latest_tag = latest_release["tag_name"]
# Get the list of assets for the latest release
github_assets = github_api_request(
f"repos/astral-sh/python-build-standalone/releases/tags/{latest_tag}"
)["assets"]
assets = [
PythonBuildStandaloneAsset(name=ga["name"], url=ga["browser_download_url"])
for ga in github_assets
if ga["name"].endswith("install_only.tar.gz")
]
# Try to keep output order stable
assets = sorted(assets, key=lambda x: x["name"])
# Write the assets to the JSON file. One day, we might need to support
# multiple releases, but for now, we only support the latest one
json_file_contents = PythonBuildStandaloneReleaseData(
releases=[
{
"tag": latest_tag,
"assets": assets,
}
]
)
with PYTHON_BUILD_STANDALONE_RELEASES.open("w", encoding="utf-8") as f:
json.dump(json_file_contents, f, indent=2)
# Add a trailing newline, our pre-commit hook requires it
f.write("\n")
print(
f"Updated {PYTHON_BUILD_STANDALONE_RELEASES.name} with {len(assets)} assets for tag {latest_tag}"
)
if __name__ == "__main__":
main()
+240 -19
View File
@@ -1,13 +1,16 @@
#!/usr/bin/env python3
from __future__ import annotations
import copy
import difflib
import logging
import operator
import re
import tomllib
from collections.abc import Mapping, MutableMapping
from pathlib import Path
from typing import Any, Final, Literal, TypedDict, Union
from typing import Any, Final, Literal, TypedDict
from xml.etree import ElementTree as ET
import click
import requests
@@ -17,8 +20,8 @@ from packaging.version import Version
from rich.logging import RichHandler
from rich.syntax import Syntax
from cibuildwheel._compat import tomllib
from cibuildwheel.extra import dump_python_configurations
from cibuildwheel.extra import dump_python_configurations, get_pyodide_xbuildenv_info
from cibuildwheel.platforms.android import android_triplet
log = logging.getLogger("cibw")
@@ -44,13 +47,32 @@ class ConfigWinPP(TypedDict):
url: str
class ConfigMacOS(TypedDict):
class ConfigWinGP(TypedDict):
identifier: str
version: str
url: str
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
class ConfigApple(TypedDict):
identifier: str
version: str
url: str
class ConfigAndroid(TypedDict):
identifier: str
version: str
url: str
class ConfigPyodide(TypedDict):
identifier: str
version: str
default_pyodide_version: str
node_version: str
AnyConfig = ConfigWinCP | ConfigWinPP | ConfigWinGP | ConfigApple | ConfigAndroid | ConfigPyodide
# The following set of "Versions" classes allow the initial call to the APIs to
@@ -106,6 +128,76 @@ class WindowsVersions:
)
class GraalPyVersions:
def __init__(self) -> None:
response = requests.get("https://api.github.com/repos/oracle/graalpython/releases")
response.raise_for_status()
releases = response.json()
gp_version_re = re.compile(r"-(\d+\.\d+\.\d+)$")
cp_version_re = re.compile(r"Python (\d+\.\d+(?:\.\d+)?)")
for release in releases:
m = gp_version_re.search(release["tag_name"])
if m:
release["graalpy_version"] = Version(m.group(1))
m = cp_version_re.search(release["body"])
if m:
release["python_version"] = Version(m.group(1))
self.releases = [r for r in releases if "graalpy_version" in r and "python_version" in r]
def update_version(self, identifier: str, spec: Specifier) -> AnyConfig:
if "x86_64" in identifier or "amd64" in identifier:
arch = "x86_64"
elif "arm64" in identifier or "aarch64" in identifier:
arch = "aarch64"
else:
msg = f"{identifier} not supported yet on GraalPy"
raise RuntimeError(msg)
gpspec_str = identifier.split("-")[0].split("_")[1]
gpspec = Specifier("==24.2.*") if gpspec_str == "242" else Specifier(f"=={gpspec_str}.*")
releases = [r for r in self.releases if spec.contains(r["python_version"])]
releases = [r for r in self.releases if gpspec.contains(r["graalpy_version"])]
releases = sorted(releases, key=lambda r: r["graalpy_version"])
if not releases:
msg = f"GraalPy {arch} not found for {spec}!"
raise RuntimeError(msg)
release = releases[-1]
version = release["python_version"]
gpversion = release["graalpy_version"]
if "macosx" in identifier:
arch = "x86_64" if "x86_64" in identifier else "arm64"
config = ConfigApple
platform = "macos"
elif "win" in identifier:
arch = "aarch64" if "arm64" in identifier else "x86_64"
config = ConfigWinGP
platform = "windows"
else:
msg = "GraalPy provides downloads for macOS and Windows and is included for manylinux"
raise RuntimeError(msg)
arch = "amd64" if arch == "x86_64" else "aarch64"
ext = "zip" if "win" in identifier else "tar.gz"
(url,) = (
rf["browser_download_url"]
for rf in release["assets"]
if rf["name"].endswith(f"{platform}-{arch}.{ext}")
and rf["name"].startswith(f"graalpy-{gpversion.major}")
)
return config(
identifier=identifier,
version=f"{version.major}.{version.minor}",
url=url,
)
class PyPyVersions:
def __init__(self, arch_str: ArchStr):
response = requests.get("https://downloads.python.org/pypy/versions.json")
@@ -133,7 +225,7 @@ class PyPyVersions:
def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
releases = [r for r in self.releases if spec.contains(r["python_version"])]
releases = sorted(releases, key=lambda r: r["pypy_version"])
releases = sorted(releases, key=operator.itemgetter("pypy_version"))
releases = [r for r in releases if self.get_arch_file(r)]
if not releases:
@@ -154,13 +246,13 @@ class PyPyVersions:
url=url,
)
def update_version_macos(self, spec: Specifier) -> ConfigMacOS:
def update_version_macos(self, spec: Specifier) -> ConfigApple:
if self.arch not in {"64", "ARM64"}:
msg = f"'{self.arch}' arch not supported yet on macOS"
raise RuntimeError(msg)
releases = [r for r in self.releases if spec.contains(r["python_version"])]
releases = sorted(releases, key=lambda r: r["pypy_version"])
releases = sorted(releases, key=operator.itemgetter("pypy_version"))
if not releases:
msg = f"PyPy macOS {self.arch} not found for {spec}!"
@@ -178,7 +270,7 @@ class PyPyVersions:
if "" in rf["platform"] == "darwin" and rf["arch"] == arch
)
return ConfigMacOS(
return ConfigApple(
identifier=identifier,
version=f"{version.major}.{version.minor}",
url=url,
@@ -196,15 +288,19 @@ class CPythonVersions:
self.versions_dict: dict[Version, int] = {}
for release in releases_info:
# Removing the prefix, Python 3.9 would use: release["name"].removeprefix("Python ")
version = Version(release["name"][7:])
# Skip the pymanager releases
if not release["slug"].startswith("python"):
continue
# Removing the prefix
version = Version(release["name"].removeprefix("Python "))
uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
self.versions_dict[version] = uri
def update_version_macos(
self, identifier: str, version: Version, spec: Specifier
) -> ConfigMacOS | None:
) -> ConfigApple | None:
# see note above on Specifier.filter
unsorted_versions = spec.filter(self.versions_dict)
sorted_versions = sorted(unsorted_versions, reverse=True)
@@ -223,7 +319,7 @@ class CPythonVersions:
urls = [rf["url"] for rf in file_info if file_ident in rf["url"]]
if urls:
return ConfigMacOS(
return ConfigApple(
identifier=identifier,
version=f"{new_version.major}.{new_version.minor}",
url=urls[0],
@@ -232,6 +328,114 @@ class CPythonVersions:
return None
class AndroidVersions:
# This should be replaced with official python.org downloads once they're available.
MAVEN_URL = "https://repo.maven.apache.org/maven2/com/chaquo/python/python"
def __init__(self) -> None:
response = requests.get(f"{self.MAVEN_URL}/maven-metadata.xml")
response.raise_for_status()
root = ET.fromstring(response.text)
self.versions: list[Version] = []
for version_elem in root.findall("./versioning/versions/version"):
version_str = version_elem.text
assert isinstance(version_str, str), version_str
self.versions.append(Version(version_str))
def update_version_android(
self, identifier: str, version: Version, spec: Specifier
) -> ConfigAndroid | None:
sorted_versions = sorted(spec.filter(self.versions), reverse=True)
# Return a config using the highest version for the given specifier.
if sorted_versions:
max_version = sorted_versions[0]
triplet = android_triplet(identifier)
return ConfigAndroid(
identifier=identifier,
version=str(version),
url=f"{self.MAVEN_URL}/{max_version}/python-{max_version}-{triplet}.tar.gz",
)
else:
return None
class CPythonIOSVersions:
def __init__(self) -> None:
response = requests.get(
"https://api.github.com/repos/beeware/Python-Apple-support/releases",
headers={
"Accept": "application/vnd.github+json",
"X-Github-Api-Version": "2022-11-28",
},
)
response.raise_for_status()
releases_info = response.json()
self.versions_dict: dict[Version, dict[int, str]] = {}
# Each release has a name like "3.13-b4"
for release in releases_info:
py_version, build = release["name"].split("-")
version = Version(py_version)
self.versions_dict.setdefault(version, {})
# There are several release assets associated with each release;
# The name of the asset will be something like
# "Python-3.11-iOS-support.b4.tar.gz". Store all builds that are
# "-iOS-support" builds, retaining the download URL.
for asset in release["assets"]:
filename, build, _, _ = asset["name"].rsplit(".", 3)
if filename.endswith("-iOS-support"):
self.versions_dict[version][int(build[1:])] = asset["browser_download_url"]
def update_version_ios(self, identifier: str, version: Version) -> ConfigApple | None:
# Return a config using the highest build number for the given version.
urls = [url for _, url in sorted(self.versions_dict.get(version, {}).items())]
if urls:
return ConfigApple(
identifier=identifier,
version=str(version),
url=urls[-1],
)
return None
class PyodideVersions:
def __init__(self) -> None:
xbuildenv_info = get_pyodide_xbuildenv_info()
self.releases = xbuildenv_info["releases"]
def update_version_pyodide(
self, identifier: str, version: Version, spec: Specifier, node_version: str
) -> ConfigPyodide | None:
# get releases that match the python version
releases = [
r for r in self.releases.values() if spec.contains(Version(r["python_version"]))
]
# sort by version, latest first
releases.sort(key=lambda r: Version(r["version"]), reverse=True)
if not releases:
msg = f"Pyodide not found for {spec}!"
raise ValueError(msg)
final_releases = [r for r in releases if not Version(r["version"]).is_prerelease]
# prefer a final release if available, otherwise use the latest
# pre-release
release = final_releases[0] if final_releases else releases[0]
return ConfigPyodide(
identifier=identifier,
version=str(version),
default_pyodide_version=release["version"],
node_version=node_version,
)
# This is a universal interface to all the above Versions classes. Given an
# identifier, it updates a config dict.
@@ -250,6 +454,13 @@ class AllVersions:
self.macos_pypy = PyPyVersions("64")
self.macos_pypy_arm64 = PyPyVersions("ARM64")
self.android = AndroidVersions()
self.ios_cpython = CPythonIOSVersions()
self.graalpy = GraalPyVersions()
self.pyodide = PyodideVersions()
def update_config(self, config: MutableMapping[str, str]) -> None:
identifier = config["identifier"]
version = Version(config["version"])
@@ -267,6 +478,8 @@ class AllVersions:
config_update = self.macos_pypy.update_version_macos(spec)
elif "macosx_arm64" in identifier:
config_update = self.macos_pypy_arm64.update_version_macos(spec)
elif identifier.startswith("gp"):
config_update = self.graalpy.update_version(identifier, spec)
elif "t-win32" in identifier and identifier.startswith("cp"):
config_update = self.windows_t_32.update_version_windows(spec)
elif "win32" in identifier and identifier.startswith("cp"):
@@ -278,10 +491,20 @@ class AllVersions:
config_update = self.windows_64.update_version_windows(spec)
elif identifier.startswith("pp"):
config_update = self.windows_pypy_64.update_version_windows(spec)
elif identifier.startswith("gp"):
config_update = self.graalpy.update_version(identifier, spec)
elif "t-win_arm64" in identifier and identifier.startswith("cp"):
config_update = self.windows_t_arm64.update_version_windows(spec)
elif "win_arm64" in identifier and identifier.startswith("cp"):
config_update = self.windows_arm64.update_version_windows(spec)
elif "android" in identifier:
config_update = self.android.update_version_android(identifier, version, spec)
elif "ios" in identifier:
config_update = self.ios_cpython.update_version_ios(identifier, version)
elif "pyodide" in identifier:
config_update = self.pyodide.update_version_pyodide(
identifier, version, spec, config["node_version"]
)
assert config_update is not None, f"{identifier} not found!"
config.update(**config_update)
@@ -311,11 +534,9 @@ def update_pythons(force: bool, level: str) -> None:
with toml_file_path.open("rb") as f:
configs = tomllib.load(f)
for config in configs["windows"]["python_configurations"]:
all_versions.update_config(config)
for config in configs["macos"]["python_configurations"]:
all_versions.update_config(config)
for platform in ["windows", "macos", "android", "ios", "pyodide"]:
for config in configs[platform]["python_configurations"]:
all_versions.update_config(config)
result_toml = dump_python_configurations(configs)
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import re
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent / ".."
CHANGELOG_FILE = PROJECT_ROOT / "docs" / "changelog.md"
README_FILE = PROJECT_ROOT / "README.md"
# https://regexr.com/622ds
FIRST_5_CHANGELOG_ENTRIES_REGEX = re.compile(r"""(^###.*?(?=###)){5}""", re.DOTALL | re.MULTILINE)
# https://regexr.com/622e5
README_CHANGELOG_SECTION = re.compile(
r"""(?<=<!-- START bin\/update_readme_changelog.py -->\n).*(?=<!-- END bin\/update_readme_changelog.py -->)""",
re.DOTALL,
)
def main() -> None:
changelog_text = CHANGELOG_FILE.read_text()
readme_text = README_FILE.read_text()
mini_changelog_match = FIRST_5_CHANGELOG_ENTRIES_REGEX.search(changelog_text)
assert mini_changelog_match, "Failed to find the first few changelog entries"
mini_changelog = "\n".join(
[
"",
"<!-- this section was generated by bin/update_readme_changelog.py -- do not edit manually -->",
"",
mini_changelog_match.group(0).strip(),
"",
"",
]
)
if not re.search(README_CHANGELOG_SECTION, readme_text):
sys.exit("Changelog section not found in README")
readme_text = re.sub(README_CHANGELOG_SECTION, mini_changelog, readme_text)
README_FILE.write_text(readme_text)
if __name__ == "__main__":
main()
+3 -5
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
from __future__ import annotations
import dataclasses
import difflib
import logging
import subprocess
from dataclasses import dataclass
import tomllib
from pathlib import Path
from typing import Final
@@ -15,8 +15,6 @@ from packaging.version import InvalidVersion, Version
from rich.logging import RichHandler
from rich.syntax import Syntax
from cibuildwheel._compat import tomllib
log = logging.getLogger("cibw")
# Looking up the dir instead of using utils.resources_dir
@@ -30,7 +28,7 @@ GET_VIRTUALENV_URL_TEMPLATE: Final[str] = (
)
@dataclass(frozen=True, order=True)
@dataclasses.dataclass(frozen=True, order=True)
class VersionTuple:
version: Version
version_string: str
+1 -3
View File
@@ -1,3 +1 @@
from __future__ import annotations
__version__ = "2.23.4"
__version__ = "3.1.0"
+141 -94
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import argparse
import contextlib
import dataclasses
import functools
import os
import shutil
import sys
@@ -9,33 +9,24 @@ import tarfile
import textwrap
import traceback
import typing
from collections.abc import Iterable, Sequence, Set
from collections.abc import Iterable, Sequence
from pathlib import Path
from tempfile import mkdtemp
from typing import Protocol
from typing import Any, Literal, TextIO
import cibuildwheel
import cibuildwheel.linux
import cibuildwheel.macos
import cibuildwheel.pyodide
import cibuildwheel.util
import cibuildwheel.windows
from cibuildwheel import errors
from cibuildwheel._compat.typing import assert_never
from cibuildwheel.architecture import Architecture, allowed_architectures_check
from cibuildwheel.ci import CIProvider, detect_ci_provider, fix_ansi_codes_for_github_actions
from cibuildwheel.logger import log
from cibuildwheel.options import CommandLineArguments, Options, compute_options
from cibuildwheel.typing import PLATFORMS, GenericPythonConfiguration, PlatformName
from cibuildwheel.util import (
CIBW_CACHE_PATH,
BuildSelector,
CIProvider,
Unbuffered,
chdir,
detect_ci_provider,
fix_ansi_codes_for_github_actions,
strtobool,
)
from cibuildwheel.platforms import ALL_PLATFORM_MODULES, get_build_identifiers, native_platform
from cibuildwheel.selector import BuildSelector, EnableGroup, selector_matches
from cibuildwheel.typing import PLATFORMS, PlatformName
from cibuildwheel.util.file import CIBW_CACHE_PATH
from cibuildwheel.util.helpers import strtobool
from cibuildwheel.util.resources import read_all_configs
@dataclasses.dataclass
@@ -43,6 +34,29 @@ class GlobalOptions:
print_traceback_on_error: bool = True # decides what happens when errors are hit.
@dataclasses.dataclass(frozen=True)
class FileReport:
name: str
size: str
# Taken from https://stackoverflow.com/a/107717
class Unbuffered:
def __init__(self, stream: TextIO) -> None:
self.stream = stream
def write(self, data: str) -> None:
self.stream.write(data)
self.stream.flush()
def writelines(self, data: Iterable[str]) -> None:
self.stream.writelines(data)
self.stream.flush()
def __getattr__(self, attr: str) -> Any:
return getattr(self.stream, attr)
def main() -> None:
global_options = GlobalOptions()
try:
@@ -52,7 +66,7 @@ def main() -> None:
if log.step_active:
log.step_end_with_error(message)
else:
print(f"cibuildwheel: {message}", file=sys.stderr)
log.error(message)
if global_options.print_traceback_on_error:
traceback.print_exc(file=sys.stderr)
@@ -66,7 +80,10 @@ def main_inner(global_options: GlobalOptions) -> None:
rather than exiting directly.
"""
parser = argparse.ArgumentParser(
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
if sys.version_info >= (3, 14):
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
parser = make_parser(
description="Build wheels for all the platforms.",
epilog="""
Most options are supplied via environment variables or in
@@ -77,13 +94,14 @@ def main_inner(global_options: GlobalOptions) -> None:
parser.add_argument(
"--platform",
choices=["auto", "linux", "macos", "windows", "pyodide"],
choices=["auto", "linux", "macos", "windows", "pyodide", "android", "ios"],
default=None,
help="""
Platform to build for. Use this option to override the
auto-detected platform. Specifying "macos" or "windows" only works
on that operating system, but "linux" works on all three, as long
as Docker/Podman is installed. Default: auto.
Platform to build for. Use this option to override the auto-detected
platform. Specifying "macos" or "windows" only works on that
operating system. "linux" works on any desktop OS, as long as
Docker/Podman is installed. "pyodide" and "android" only work on
Linux and macOS. "ios" only works on macOS. Default: auto.
""",
)
@@ -101,9 +119,22 @@ def main_inner(global_options: GlobalOptions) -> None:
""",
)
enable_groups_str = ", ".join(g.value for g in EnableGroup)
parser.add_argument(
"--enable",
action="append",
default=[],
metavar="GROUP",
help=f"""
Enable an additional category of builds. Use multiple times to select multiple groups. Choices: {enable_groups_str}.
""",
)
parser.add_argument(
"--only",
default=None,
choices=[v["identifier"] for vv in read_all_configs().values() for v in vv],
metavar="IDENTIFIER",
help="""
Force a single wheel build when given an identifier. Overrides
CIBW_BUILD/CIBW_SKIP. --platform and --arch cannot be specified
@@ -131,7 +162,7 @@ def main_inner(global_options: GlobalOptions) -> None:
parser.add_argument(
"package_dir",
metavar="PACKAGE",
default=Path("."),
default=Path(),
type=Path,
nargs="?",
help="""
@@ -157,12 +188,6 @@ def main_inner(global_options: GlobalOptions) -> None:
help="Do not report an error code if the build does not match any wheels.",
)
parser.add_argument(
"--prerelease-pythons",
action="store_true",
help="Enable pre-release Python versions if available.",
)
parser.add_argument(
"--debug-traceback",
action="store_true",
@@ -200,7 +225,7 @@ def main_inner(global_options: GlobalOptions) -> None:
# This is now the new package dir
args.package_dir = project_dir.resolve()
with chdir(project_dir):
with contextlib.chdir(project_dir):
build_in_directory(args)
finally:
# avoid https://github.com/python/cpython/issues/86962 by performing
@@ -219,26 +244,14 @@ def _compute_platform_only(only: str) -> PlatformName:
return "windows"
if "pyodide_" in only:
return "pyodide"
if "android_" in only:
return "android"
if "ios_" in only:
return "ios"
msg = f"Invalid --only='{only}', must be a build selector with a known platform"
raise errors.ConfigurationError(msg)
def _compute_platform_auto() -> PlatformName:
if sys.platform.startswith("linux"):
return "linux"
elif sys.platform == "darwin":
return "macos"
elif sys.platform == "win32":
return "windows"
else:
msg = (
'cibuildwheel: Unable to detect platform from "sys.platform". cibuildwheel doesn\'t '
"support building wheels for this platform. You might be able to build for a different "
"platform using the --platform argument. Check --help output for more information."
)
raise errors.ConfigurationError(msg)
def _compute_platform(args: CommandLineArguments) -> PlatformName:
platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "") or "auto"
@@ -258,37 +271,14 @@ def _compute_platform(args: CommandLineArguments) -> PlatformName:
elif platform_option_value != "auto":
return typing.cast(PlatformName, platform_option_value)
return _compute_platform_auto()
class PlatformModule(Protocol):
# note that as per PEP544, the self argument is ignored when the protocol
# is applied to a module
def get_python_configurations(
self, build_selector: BuildSelector, architectures: Set[Architecture]
) -> Sequence[GenericPythonConfiguration]: ...
def build(self, options: Options, tmp_path: Path) -> None: ...
def get_platform_module(platform: PlatformName) -> PlatformModule:
if platform == "linux":
return cibuildwheel.linux
if platform == "windows":
return cibuildwheel.windows
if platform == "macos":
return cibuildwheel.macos
if platform == "pyodide":
return cibuildwheel.pyodide
assert_never(platform)
return native_platform()
def build_in_directory(args: CommandLineArguments) -> None:
platform: PlatformName = _compute_platform(args)
if platform == "pyodide" and sys.platform == "win32":
msg = "cibuildwheel: Building for pyodide is not supported on Windows"
print(msg, file=sys.stderr)
sys.exit(2)
msg = "Building for pyodide is not supported on Windows"
raise errors.ConfigurationError(msg)
options = compute_options(platform=platform, command_line_arguments=args, env=os.environ)
@@ -300,7 +290,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
msg = f"Could not find any of {{{names}}} at root of package"
raise errors.ConfigurationError(msg)
platform_module = get_platform_module(platform)
platform_module = ALL_PLATFORM_MODULES[platform]
identifiers = get_build_identifiers(
platform_module=platform_module,
build_selector=options.globals.build_selector,
@@ -340,14 +330,11 @@ def build_in_directory(args: CommandLineArguments) -> None:
output_dir = options.globals.output_dir
if not output_dir.exists():
output_dir.mkdir(parents=True)
output_dir.mkdir(parents=True, exist_ok=True)
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
try:
with cibuildwheel.util.print_new_wheels(
"\n{n} wheels produced in {m:.0f} minutes:", output_dir
):
with log.print_summary(options=options):
platform_module.build(options, tmp_path)
finally:
# avoid https://github.com/python/cpython/issues/86962 by performing
@@ -389,15 +376,6 @@ def print_preamble(platform: str, options: Options, identifiers: Sequence[str])
print("Here we go!\n")
def get_build_identifiers(
platform_module: PlatformModule,
build_selector: BuildSelector,
architectures: Set[Architecture],
) -> list[str]:
python_configurations = platform_module.get_python_configurations(build_selector, architectures)
return [config.identifier for config in python_configurations]
def detect_warnings(*, options: Options, identifiers: Iterable[str]) -> list[str]:
warnings = []
@@ -418,9 +396,78 @@ def detect_warnings(*, options: Options, identifiers: Iterable[str]) -> list[str
if any(o and ("{python}" in o or "{pip}" in o) for o in option_values):
# Reminder: in an f-string, double braces means literal single brace
msg = (
f"{option_name}: '{{python}}' and '{{pip}}' are no longer needed, "
"and will be removed in cibuildwheel 3. Simply use 'python' or 'pip' instead."
f"{option_name}: '{{python}}' and '{{pip}}' are no longer supported "
"and have been removed in cibuildwheel 3. Simply use 'python' or 'pip' instead."
)
raise errors.ConfigurationError(msg)
build_selector = options.globals.build_selector
test_selector = options.globals.test_selector
all_valid_identifiers = [
config.identifier
for module in ALL_PLATFORM_MODULES.values()
for config in module.all_python_configurations()
]
enabled_selector = BuildSelector(
build_config="*", skip_config="", enable=options.globals.build_selector.enable
)
all_enabled_identifiers = [
identifier for identifier in all_valid_identifiers if enabled_selector(identifier)
]
warnings += check_for_invalid_selectors(
selector_name="build",
selector_value=build_selector.build_config,
all_valid_identifiers=all_valid_identifiers,
all_enabled_identifiers=all_enabled_identifiers,
)
warnings += check_for_invalid_selectors(
selector_name="skip",
selector_value=build_selector.skip_config,
all_valid_identifiers=all_valid_identifiers,
all_enabled_identifiers=all_enabled_identifiers,
)
warnings += check_for_invalid_selectors(
selector_name="test_skip",
selector_value=test_selector.skip_config,
all_valid_identifiers=all_valid_identifiers,
all_enabled_identifiers=all_enabled_identifiers,
)
return warnings
def check_for_invalid_selectors(
*,
selector_name: Literal["build", "skip", "test_skip"],
selector_value: str,
all_valid_identifiers: Sequence[str],
all_enabled_identifiers: Sequence[str],
) -> list[str]:
warnings = []
for selector in selector_value.split():
if not any(selector_matches(selector, i) for i in all_enabled_identifiers):
msg = f"Invalid {selector_name} selector: {selector!r}. "
error_type: type = errors.ConfigurationError
if any(selector_matches(selector, i) for i in all_valid_identifiers):
msg += "This selector matches a group that wasn't enabled. Enable it using the `enable` option or remove this selector. "
if "p2" in selector or "p35" in selector:
msg += f"cibuildwheel 3.x no longer supports Python < 3.8. Please use the 1.x series or update `{selector_name}`. "
error_type = errors.DeprecationError
if "p36" in selector or "p37" in selector:
msg += f"cibuildwheel 3.x no longer supports Python < 3.8. Please use the 2.x series or update `{selector_name}`. "
error_type = errors.DeprecationError
if selector_name == "build":
raise error_type(msg)
msg += "This selector will have no effect. "
warnings.append(msg)
return warnings
-1
View File
@@ -1 +0,0 @@
from __future__ import annotations
-10
View File
@@ -1,10 +0,0 @@
from __future__ import annotations
import sys
if sys.version_info >= (3, 11):
from tomllib import load, loads
else:
from tomli import load, loads
__all__ = ["load", "loads"]
-14
View File
@@ -1,14 +0,0 @@
from __future__ import annotations
import sys
if sys.version_info < (3, 11):
from typing_extensions import NotRequired, Self, assert_never
else:
from typing import NotRequired, Self, assert_never
__all__ = (
"NotRequired",
"Self",
"assert_never",
)
+134 -70
View File
@@ -1,14 +1,15 @@
from __future__ import annotations
import functools
import platform as platform_module
import re
import shutil
import subprocess
import sys
import typing
from collections.abc import Set
from enum import Enum
from enum import StrEnum, auto
from typing import Final, Literal
from ._compat.typing import assert_never
from cibuildwheel import errors
from .typing import PlatformName
PRETTY_NAMES: Final[dict[PlatformName, str]] = {
@@ -16,72 +17,100 @@ PRETTY_NAMES: Final[dict[PlatformName, str]] = {
"macos": "macOS",
"windows": "Windows",
"pyodide": "Pyodide",
"android": "Android",
"ios": "iOS",
}
ARCH_SYNONYMS: Final[list[dict[PlatformName, str | None]]] = [
{"linux": "x86_64", "macos": "x86_64", "windows": "AMD64"},
{"linux": "x86_64", "macos": "x86_64", "windows": "AMD64", "android": "x86_64"},
{"linux": "i686", "macos": None, "windows": "x86"},
{"linux": "aarch64", "macos": "arm64", "windows": "ARM64"},
{"linux": "aarch64", "macos": "arm64", "windows": "ARM64", "android": "arm64_v8a"},
]
@functools.total_ordering
class Architecture(Enum):
value: str
def arch_synonym(arch: str, from_platform: PlatformName, to_platform: PlatformName) -> str | None:
for arch_synonym_ in ARCH_SYNONYMS:
if arch == arch_synonym_.get(from_platform):
return arch_synonym_.get(to_platform, arch)
# mac/linux archs
x86_64 = "x86_64"
return arch
def _check_aarch32_el0() -> bool:
"""Check if running armv7l natively on aarch64 is supported"""
if not sys.platform.startswith("linux"):
return False
if platform_module.machine() != "aarch64":
return False
executable = shutil.which("linux32")
if executable is None:
return False
check = subprocess.run([executable, "uname", "-m"], check=False, capture_output=True, text=True)
return check.returncode == 0 and check.stdout.startswith("armv")
@typing.final
class Architecture(StrEnum):
# mac/linux/android archs
x86_64 = auto()
# linux archs
i686 = "i686"
aarch64 = "aarch64"
ppc64le = "ppc64le"
s390x = "s390x"
armv7l = "armv7l"
i686 = auto()
aarch64 = auto()
ppc64le = auto()
s390x = auto()
armv7l = auto()
riscv64 = auto()
# mac archs
universal2 = "universal2"
arm64 = "arm64"
universal2 = auto()
arm64 = auto()
# windows archs
x86 = "x86"
x86 = auto()
AMD64 = "AMD64"
ARM64 = "ARM64"
# WebAssembly
wasm32 = "wasm32"
wasm32 = auto()
# Allow this to be sorted
def __lt__(self, other: Architecture) -> bool:
return self.value < other.value
# android archs
arm64_v8a = auto()
def __str__(self) -> str:
return self.name
# iOS "multiarch" architectures that include both
# the CPU architecture and the ABI.
arm64_iphoneos = auto()
arm64_iphonesimulator = auto()
x86_64_iphonesimulator = auto()
@staticmethod
def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
def parse_config(config: str, platform: PlatformName) -> "set[Architecture]":
result = set()
for arch_str in re.split(r"[\s,]+", config):
if arch_str == "auto":
result |= Architecture.auto_archs(platform=platform)
elif arch_str == "native":
native_arch = Architecture.native_arch(platform=platform)
if native_arch:
result.add(native_arch)
elif arch_str == "all":
result |= Architecture.all_archs(platform=platform)
elif arch_str == "auto64":
result |= Architecture.bitness_archs(platform=platform, bitness="64")
elif arch_str == "auto32":
result |= Architecture.bitness_archs(platform=platform, bitness="32")
else:
result.add(Architecture(arch_str))
match arch_str:
case "auto":
result |= Architecture.auto_archs(platform=platform)
case "native":
if native_arch := Architecture.native_arch(platform=platform):
result.add(native_arch)
case "all":
result |= Architecture.all_archs(platform=platform)
case "auto64":
result |= Architecture.bitness_archs(platform=platform, bitness="64")
case "auto32":
result |= Architecture.bitness_archs(platform=platform, bitness="32")
case _:
try:
result.add(Architecture(arch_str))
except ValueError as e:
msg = f"Invalid architecture '{arch_str}'"
raise errors.ConfigurationError(msg) from e
return result
@staticmethod
def native_arch(platform: PlatformName) -> Architecture | None:
if platform == "pyodide":
return Architecture.wasm32
def native_arch(platform: PlatformName) -> "Architecture | None":
native_machine = platform_module.machine()
native_architecture = Architecture(native_machine)
# Cross-platform support. Used for --print-build-identifiers or docker builds.
host_platform: PlatformName = (
@@ -90,42 +119,49 @@ class Architecture(Enum):
else ("macos" if sys.platform.startswith("darwin") else "linux")
)
native_machine = platform_module.machine()
native_architecture = Architecture(native_machine)
if platform == "pyodide":
return Architecture.wasm32
elif platform == "ios":
# Can only build for iOS on macOS. The "native" architecture is the
# simulator for the macOS native platform.
if host_platform == "macos":
if native_architecture == Architecture.x86_64:
return Architecture.x86_64_iphonesimulator
else:
return Architecture.arm64_iphonesimulator
else:
return None
# we might need to rename the native arch to the machine we're running
# on, as the same arch can have different names on different platforms
if host_platform != platform:
for arch_synonym in ARCH_SYNONYMS:
if native_machine == arch_synonym.get(host_platform):
synonym = arch_synonym[platform]
synonym = arch_synonym(native_machine, host_platform, platform)
if synonym is None:
# can't build anything on this platform
return None
if synonym is None:
# can't build anything on this platform
return None
native_architecture = Architecture(synonym)
native_architecture = Architecture(synonym)
return native_architecture
@staticmethod
def auto_archs(platform: PlatformName) -> set[Architecture]:
def auto_archs(platform: PlatformName) -> "set[Architecture]":
native_arch = Architecture.native_arch(platform)
if native_arch is None:
return set() # can't build anything on this platform
result = {native_arch}
if platform == "linux" and Architecture.x86_64 in result:
# x86_64 machines can run i686 containers
result.add(Architecture.i686)
if platform == "windows" and Architecture.AMD64 in result:
result.add(Architecture.x86)
match platform:
case "windows" if Architecture.AMD64 in result:
result.add(Architecture.x86)
case "ios" if native_arch == Architecture.arm64_iphonesimulator:
# Also build the device wheel if we're on ARM64.
result.add(Architecture.arm64_iphoneos)
return result
@staticmethod
def all_archs(platform: PlatformName) -> set[Architecture]:
def all_archs(platform: PlatformName) -> "set[Architecture]":
all_archs_map = {
"linux": {
Architecture.x86_64,
@@ -134,23 +170,51 @@ class Architecture(Enum):
Architecture.ppc64le,
Architecture.s390x,
Architecture.armv7l,
Architecture.riscv64,
},
"macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2},
"windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64},
"pyodide": {Architecture.wasm32},
"android": {Architecture.x86_64, Architecture.arm64_v8a},
"ios": {
Architecture.x86_64_iphonesimulator,
Architecture.arm64_iphonesimulator,
Architecture.arm64_iphoneos,
},
}
return all_archs_map[platform]
@staticmethod
def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> set[Architecture]:
archs_32 = {Architecture.i686, Architecture.x86, Architecture.armv7l}
auto_archs = Architecture.auto_archs(platform)
def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> "set[Architecture]":
# This map maps 64-bit architectures to their 32-bit equivalents.
archs_map = {
Architecture.x86_64: Architecture.i686,
Architecture.AMD64: Architecture.x86,
Architecture.aarch64: Architecture.armv7l,
}
native_arch = Architecture.native_arch(platform)
if bitness == "64":
return auto_archs - archs_32
if bitness == "32":
return auto_archs & archs_32
assert_never(bitness)
if native_arch is None:
return set() # can't build anything on this platform
if native_arch == Architecture.wasm32:
return {native_arch} if bitness == "32" else set()
match bitness:
case "64":
return {native_arch} if native_arch not in archs_map.values() else set()
case "32":
if native_arch in archs_map.values():
return {native_arch}
elif native_arch in archs_map and platform in {"linux", "windows"}:
if native_arch == Architecture.aarch64 and not _check_aarch32_el0():
# If we're on aarch64, skip if we cannot build armv7l wheels.
return set()
return {archs_map[native_arch]}
else:
return set()
case _:
typing.assert_never(bitness)
def allowed_architectures_check(
+9 -7
View File
@@ -1,21 +1,23 @@
from __future__ import annotations
import dataclasses
import subprocess
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass
from typing import Callable, Dict, List # noqa: TID251
from collections.abc import (
Callable,
Iterable,
Mapping,
Sequence,
)
import bashlex
# a function that takes a command and the environment, and returns the result
EnvironmentExecutor = Callable[[List[str], Dict[str, str]], str]
EnvironmentExecutor = Callable[[list[str], dict[str, str]], str]
def local_environment_executor(command: Sequence[str], env: Mapping[str, str]) -> str:
return subprocess.run(command, env=env, text=True, stdout=subprocess.PIPE, check=True).stdout
@dataclass(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class NodeExecutionContext:
environment: dict[str, str]
input: str
+79
View File
@@ -0,0 +1,79 @@
import os
import re
from enum import Enum
from .util.helpers import strtobool
ANSI_CODE_REGEX = re.compile(r"(\033\[[0-9;]*m)")
class CIProvider(Enum):
# official support
travis_ci = "travis"
circle_ci = "circle_ci"
azure_pipelines = "azure_pipelines"
github_actions = "github_actions"
gitlab = "gitlab"
cirrus_ci = "cirrus_ci"
# unofficial support
appveyor = "appveyor"
other = "other"
def detect_ci_provider() -> CIProvider | None:
if "TRAVIS" in os.environ:
return CIProvider.travis_ci
elif "APPVEYOR" in os.environ:
return CIProvider.appveyor
elif "CIRCLECI" in os.environ:
return CIProvider.circle_ci
elif "AZURE_HTTP_USER_AGENT" in os.environ:
return CIProvider.azure_pipelines
elif "GITHUB_ACTIONS" in os.environ:
return CIProvider.github_actions
elif "GITLAB_CI" in os.environ:
return CIProvider.gitlab
elif "CIRRUS_CI" in os.environ:
return CIProvider.cirrus_ci
elif strtobool(os.environ.get("CI", "false")):
return CIProvider.other
else:
return None
def fix_ansi_codes_for_github_actions(text: str) -> str:
"""
Github Actions forgets the current ANSI style on every new line. This
function repeats the current ANSI style on every new line.
"""
ansi_codes: list[str] = []
output = ""
for line in text.splitlines(keepends=True):
# add the current ANSI codes to the beginning of the line
output += "".join(ansi_codes) + line
# split the line at each ANSI code
parts = ANSI_CODE_REGEX.split(line)
# if there are any ANSI codes, save them
if len(parts) > 1:
# iterate over the ANSI codes in this line
for code in parts[1::2]:
if code == "\033[0m":
# reset the list of ANSI codes when the clear code is found
ansi_codes = []
else:
ansi_codes.append(code)
return output
def filter_ansi_codes(text: str, /) -> str:
"""
Remove ANSI codes from text.
"""
return ANSI_CODE_REGEX.sub("", text)
+5 -8
View File
@@ -1,5 +1,3 @@
from __future__ import annotations
import dataclasses
from collections.abc import Mapping, Sequence
from typing import Any, Protocol
@@ -75,12 +73,16 @@ class EnvironmentAssignmentRaw:
return self.value
@dataclasses.dataclass
class EnvironmentAssignmentBash:
"""
An environment variable, in bash syntax. The value can use bash constructs
like "$OTHER_VAR" and "$(command arg1 arg2)".
"""
name: str
value: str
def __init__(self, assignment: str):
name, equals, value = assignment.partition("=")
if not equals:
@@ -98,13 +100,8 @@ class EnvironmentAssignmentBash:
def __repr__(self) -> str:
return f"{self.name}={self.value}"
def __eq__(self, other: object) -> bool:
if isinstance(other, EnvironmentAssignmentBash):
return self.name == other.name and self.value == other.value
return False
@dataclasses.dataclass
@dataclasses.dataclass(kw_only=True)
class ParsedEnvironment:
assignments: list[EnvironmentAssignment]
+18
View File
@@ -85,3 +85,21 @@ class RepairStepProducedNoWheelError(FatalError):
)
super().__init__(message)
self.return_code = 8
class RepairStepProducedMultipleWheelsError(FatalError):
def __init__(self, wheels: list[str]) -> None:
message = textwrap.dedent(
f"""
Build failed because the repair step completed successfully but
produced multiple wheels: {wheels}
Your `repair-wheel-command` is expected to place one repaired
wheel in the {{dest_dir}} directory. See the documentation for
example configurations:
https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command
"""
)
super().__init__(message)
self.return_code = 8
+70 -3
View File
@@ -2,11 +2,16 @@
These are utilities for the `/bin` scripts, not for the `cibuildwheel` program.
"""
from __future__ import annotations
import json
import time
import typing
import urllib.error
import urllib.request
from collections.abc import Mapping, Sequence
from io import StringIO
from typing import Protocol
from typing import Any, NotRequired, Protocol
from cibuildwheel import __version__ as cibw_version
__all__ = ("Printable", "dump_python_configurations")
@@ -32,3 +37,65 @@ def dump_python_configurations(
output.write("\n")
# Strip the final newline, to avoid two blank lines at the end.
return output.getvalue()[:-1]
def _json_request(request: urllib.request.Request, timeout: int = 30) -> dict[str, Any]:
with urllib.request.urlopen(request, timeout=timeout) as response:
return typing.cast(dict[str, Any], json.load(response))
def github_api_request(path: str, *, max_retries: int = 3) -> dict[str, Any]:
"""
Makes a GitHub API request to the given path and returns the JSON response.
"""
api_url = f"https://api.github.com/{path}"
headers = {
"Accept": "application/vnd.github.v3+json",
"User-Agent": f"cibuildwheel/{cibw_version}",
}
request = urllib.request.Request(api_url, headers=headers)
for retry_count in range(max_retries):
try:
return _json_request(request)
except (urllib.error.URLError, TimeoutError) as e:
# pylint: disable=E1101
if (
isinstance(e, urllib.error.HTTPError)
and (e.code == 403 or e.code == 429)
and e.headers.get("x-ratelimit-remaining") == "0"
):
reset_time = int(e.headers.get("x-ratelimit-reset", 0))
wait_time = max(0, reset_time - int(e.headers.get("date", 0)))
print(f"Github rate limit exceeded. Waiting for {wait_time} seconds.")
time.sleep(wait_time)
else:
print(f"Retrying GitHub API request due to error: {e}")
if retry_count == max_retries - 1:
print(f"GitHub API request failed (Network error: {e}). Check network connection.")
raise e
# Should never be reached but to keep the type checker happy
msg = "Unexpected execution path in github_api_request"
raise RuntimeError(msg)
class PyodideXBuildEnvRelease(typing.TypedDict):
version: str
python_version: str
emscripten_version: str
min_pyodide_build_version: NotRequired[str]
max_pyodide_build_version: NotRequired[str]
class PyodideXBuildEnvInfo(typing.TypedDict):
releases: dict[str, PyodideXBuildEnvRelease]
def get_pyodide_xbuildenv_info() -> PyodideXBuildEnvInfo:
xbuildenv_info_url = (
"https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json"
)
with urllib.request.urlopen(xbuildenv_info_url) as response:
return typing.cast(PyodideXBuildEnvInfo, json.loads(response.read().decode("utf-8")))
+84
View File
@@ -0,0 +1,84 @@
import dataclasses
import shlex
import typing
from collections.abc import Sequence
from typing import Literal, Self, get_args
from .logger import log
from .util.helpers import parse_key_value_string
BuildFrontendName = Literal["pip", "build", "build[uv]"]
@dataclasses.dataclass(frozen=True)
class BuildFrontendConfig:
name: BuildFrontendName
args: Sequence[str] = ()
@classmethod
def from_config_string(cls, config_string: str) -> Self:
config_dict = parse_key_value_string(config_string, ["name"], ["args"])
name = " ".join(config_dict["name"])
if name not in get_args(BuildFrontendName):
names = ", ".join(repr(n) for n in get_args(BuildFrontendName))
msg = f"Unrecognised build frontend {name!r}, must be one of {names}"
raise ValueError(msg)
name = typing.cast(BuildFrontendName, name)
args = config_dict.get("args") or []
return cls(name=name, args=args)
def options_summary(self) -> str | dict[str, str]:
if not self.args:
return self.name
else:
return {"name": self.name, "args": repr(self.args)}
def _get_verbosity_flags(level: int, frontend: BuildFrontendName) -> list[str]:
if level < 0:
if frontend == "pip":
return ["-" + -level * "q"]
msg = f"build_verbosity {level} is not supported for {frontend} frontend. Ignoring."
log.warning(msg)
if level > 0:
if frontend == "pip":
return ["-" + level * "v"]
if level > 1:
return ["-" + (level - 1) * "v"]
return []
def _split_config_settings(config_settings: str) -> list[str]:
config_settings_list = shlex.split(config_settings)
return [f"-C{setting}" for setting in config_settings_list]
# Based on build.__main__.main.
def parse_config_settings(config_settings_str: str) -> dict[str, str | list[str]]:
config_settings: dict[str, str | list[str]] = {}
for arg in shlex.split(config_settings_str):
setting, _, value = arg.partition("=")
existing_value = config_settings.get(setting)
if existing_value is None:
config_settings[setting] = value
elif isinstance(existing_value, str):
config_settings[setting] = [existing_value, value]
else:
existing_value.append(value)
return config_settings
def get_build_frontend_extra_flags(
build_frontend: BuildFrontendConfig, verbosity_level: int, config_settings: str
) -> list[str]:
return [
*_split_config_settings(config_settings),
*build_frontend.args,
*_get_verbosity_flags(verbosity_level, build_frontend.name),
]
+201 -57
View File
@@ -1,15 +1,26 @@
from __future__ import annotations
import codecs
import contextlib
import dataclasses
import functools
import hashlib
import io
import os
import re
import sys
import textwrap
import time
from typing import IO, AnyStr, Final, Tuple
from collections.abc import Generator
from pathlib import Path
from typing import IO, TYPE_CHECKING, AnyStr, Final, Literal
from .util import CIProvider, detect_ci_provider
import humanize
FoldPattern = Tuple[str, str]
from .ci import CIProvider, detect_ci_provider, filter_ansi_codes
if TYPE_CHECKING:
from .options import Options
FoldPattern = tuple[str, str]
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
FOLD_PATTERNS: Final[dict[str, FoldPattern]] = {
"azure": ("##[group]{name}", "##[endgroup]"),
@@ -24,12 +35,14 @@ PLATFORM_IDENTIFIER_DESCRIPTIONS: Final[dict[str, str]] = {
"manylinux_ppc64le": "manylinux ppc64le",
"manylinux_s390x": "manylinux s390x",
"manylinux_armv7l": "manylinux armv7l",
"manylinux_riscv64": "manylinux riscv64",
"musllinux_x86_64": "musllinux x86_64",
"musllinux_i686": "musllinux i686",
"musllinux_aarch64": "musllinux aarch64",
"musllinux_ppc64le": "musllinux ppc64le",
"musllinux_s390x": "musllinux s390x",
"musllinux_armv7l": "musllinux armv7l",
"musllinux_riscv64": "musllinux riscv64",
"win32": "Windows 32bit",
"win_amd64": "Windows 64bit",
"win_arm64": "Windows on ARM 64bit",
@@ -37,17 +50,76 @@ PLATFORM_IDENTIFIER_DESCRIPTIONS: Final[dict[str, str]] = {
"macosx_universal2": "macOS Universal 2 - x86_64 and arm64",
"macosx_arm64": "macOS arm64 - Apple Silicon",
"pyodide_wasm32": "Pyodide",
"android_arm64_v8a": "Android arm64_v8a",
"android_x86_64": "Android x86_64",
"ios_arm64_iphoneos": "iOS Device (ARM64)",
"ios_arm64_iphonesimulator": "iOS Simulator (ARM64)",
"ios_x86_64_iphonesimulator": "iOS Simulator (x86_64)",
}
class Colors:
def __init__(self, *, enabled: bool) -> None:
self.red = "\033[31m" if enabled else ""
self.green = "\033[32m" if enabled else ""
self.yellow = "\033[33m" if enabled else ""
self.blue = "\033[34m" if enabled else ""
self.cyan = "\033[36m" if enabled else ""
self.bright_red = "\033[91m" if enabled else ""
self.bright_green = "\033[92m" if enabled else ""
self.white = "\033[37m\033[97m" if enabled else ""
self.gray = "\033[38;5;244m" if enabled else ""
self.bg_grey = "\033[48;5;235m" if enabled else ""
self.bold = "\033[1m" if enabled else ""
self.faint = "\033[2m" if enabled else ""
self.end = "\033[0m" if enabled else ""
class Symbols:
def __init__(self, *, unicode: bool) -> None:
self.done = "" if unicode else "done"
self.error = "" if unicode else "failed"
@dataclasses.dataclass(kw_only=True, frozen=True)
class BuildInfo:
identifier: str
filename: Path | None
duration: float
@functools.cached_property
def size(self) -> str | None:
if self.filename is None:
return None
return humanize.naturalsize(self.filename.stat().st_size)
@functools.cached_property
def sha256(self) -> str | None:
if self.filename is None:
return None
with self.filename.open("rb") as f:
digest = hashlib.file_digest(f, "sha256")
return digest.hexdigest()
def __str__(self) -> str:
duration = humanize.naturaldelta(self.duration)
if self.filename:
return f"{self.identifier}: {self.filename.name} {self.size} in {duration}, SHA256={self.sha256}"
return f"{self.identifier}: {duration} (test only)"
class Logger:
fold_mode: str
fold_mode: Literal["azure", "github", "travis", "disabled"]
colors_enabled: bool
unicode_enabled: bool
active_build_identifier: str | None = None
build_start_time: float | None = None
step_start_time: float | None = None
active_fold_group_name: str | None = None
summary: list[BuildInfo]
def __init__(self) -> None:
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
@@ -59,25 +131,28 @@ class Logger:
ci_provider = detect_ci_provider()
if ci_provider == CIProvider.azure_pipelines:
self.fold_mode = "azure"
self.colors_enabled = True
match ci_provider:
case CIProvider.azure_pipelines:
self.fold_mode = "azure"
self.colors_enabled = True
elif ci_provider == CIProvider.github_actions:
self.fold_mode = "github"
self.colors_enabled = True
case CIProvider.github_actions:
self.fold_mode = "github"
self.colors_enabled = True
elif ci_provider == CIProvider.travis_ci:
self.fold_mode = "travis"
self.colors_enabled = True
case CIProvider.travis_ci:
self.fold_mode = "travis"
self.colors_enabled = True
elif ci_provider == CIProvider.appveyor:
self.fold_mode = "disabled"
self.colors_enabled = True
case CIProvider.appveyor:
self.fold_mode = "disabled"
self.colors_enabled = True
else:
self.fold_mode = "disabled"
self.colors_enabled = file_supports_color(sys.stdout)
case _:
self.fold_mode = "disabled"
self.colors_enabled = file_supports_color(sys.stdout)
self.summary = []
def build_start(self, identifier: str) -> None:
self.step_end()
@@ -91,7 +166,7 @@ class Logger:
self.build_start_time = time.time()
self.active_build_identifier = identifier
def build_end(self) -> None:
def build_end(self, filename: Path | None) -> None:
assert self.build_start_time is not None
assert self.active_build_identifier is not None
self.step_end()
@@ -99,11 +174,14 @@ class Logger:
c = self.colors
s = self.symbols
duration = time.time() - self.build_start_time
duration_str = humanize.naturaldelta(duration, minimum_unit="milliseconds")
print()
print(
f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration:.2f}s"
print(f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration_str}")
self.summary.append(
BuildInfo(identifier=self.active_build_identifier, filename=filename, duration=duration)
)
self.build_start_time = None
self.active_build_identifier = None
@@ -118,6 +196,7 @@ class Logger:
c = self.colors
s = self.symbols
duration = time.time() - self.step_start_time
if success:
print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78))
else:
@@ -135,24 +214,44 @@ class Logger:
def notice(self, message: str) -> None:
if self.fold_mode == "github":
print(f"::notice::{message}\n", file=sys.stderr)
print(f"::notice::cibuildwheel: {message}\n", file=sys.stderr)
else:
c = self.colors
print(f"{c.bold}Note{c.end}: {message}\n", file=sys.stderr)
print(f"cibuildwheel: {c.bold}note{c.end}: {message}\n", file=sys.stderr)
def warning(self, message: str) -> None:
if self.fold_mode == "github":
print(f"::warning::{message}\n", file=sys.stderr)
print(f"::warning::cibuildwheel: {message}\n", file=sys.stderr)
else:
c = self.colors
print(f"{c.yellow}Warning{c.end}: {message}\n", file=sys.stderr)
print(f"cibuildwheel: {c.yellow}warning{c.end}: {message}\n", file=sys.stderr)
def error(self, error: BaseException | str) -> None:
if self.fold_mode == "github":
print(f"::error::{error}\n", file=sys.stderr)
print(f"::error::cibuildwheel: {error}\n", file=sys.stderr)
else:
c = self.colors
print(f"{c.bright_red}Error{c.end}: {error}\n", file=sys.stderr)
print(f"cibuildwheel: {c.bright_red}error{c.end}: {error}\n", file=sys.stderr)
@contextlib.contextmanager
def print_summary(self, *, options: "Options") -> Generator[None, None, None]:
start = time.time()
yield
duration = time.time() - start
if summary_path := os.environ.get("GITHUB_STEP_SUMMARY"):
github_summary = self._github_step_summary(duration=duration, options=options)
Path(summary_path).write_text(filter_ansi_codes(github_summary), encoding="utf-8")
n = len(self.summary)
s = "s" if n > 1 else ""
duration_str = humanize.naturaldelta(duration)
print()
self._start_fold_group(f"{n} wheel{s} produced in {duration_str}")
for build_info in self.summary:
print(" ", build_info)
self._end_fold_group()
self.summary = []
@property
def step_active(self) -> bool:
@@ -193,6 +292,72 @@ class Logger:
# lowercase, shorten
return identifier.lower()[:20]
def _github_step_summary(self, duration: float, options: "Options") -> str:
"""
Returns the GitHub step summary, in markdown format.
"""
out = io.StringIO()
options_summary = options.summary(
identifiers=[bi.identifier for bi in self.summary], skip_unset=True
)
out.write(
textwrap.dedent("""\
### 🎡 cibuildwheel
<details>
<summary>
Build options
</summary>
```yaml
{options_summary}
```
</details>
""").format(options_summary=options_summary)
)
n_wheels = len([b for b in self.summary if b.filename])
wheel_rows = "\n".join(
"<tr>"
f"<td nowrap>{'<samp>' + b.filename.name + '</samp>' if b.filename else '*Build only*'}</td>"
f"<td nowrap>{b.size or 'N/A'}</td>"
f"<td nowrap><samp>{b.identifier}</samp></td>"
f"<td nowrap>{humanize.naturaldelta(b.duration)}</td>"
f"<td nowrap><samp>{b.sha256 or 'N/A'}</samp></td>"
"</tr>"
for b in self.summary
)
out.write(
textwrap.dedent("""\
<table>
<thead>
<tr>
<th align="left">Wheel</th>
<th align="left">Size</th>
<th align="left">Build identifier</th>
<th align="left">Time</th>
<th align="left">SHA256</th>
</tr>
</thead>
<tbody>
{wheel_rows}
</tbody>
</table>
<div align="right"><sup>{n} wheel{s} created in {duration_str}</sup></div>
""").format(
wheel_rows=wheel_rows,
n=n_wheels,
duration_str=humanize.naturaldelta(duration),
s="s" if n_wheels > 1 else "",
)
)
out.write("\n")
out.write("---")
out.write("\n")
return out.getvalue()
@property
def colors(self) -> Colors:
return Colors(enabled=self.colors_enabled)
@@ -208,17 +373,22 @@ def build_description_from_identifier(identifier: str) -> str:
build_description = ""
python_interpreter = python_identifier[0:2]
python_version = python_identifier[2:]
version_parts = python_identifier[2:].split("_")
python_version = version_parts[0]
if python_interpreter == "cp":
build_description += "CPython"
elif python_interpreter == "pp":
build_description += "PyPy"
elif python_interpreter == "gp":
build_description += "GraalPy"
else:
msg = f"unknown python {python_interpreter!r}"
raise Exception(msg)
build_description += f" {python_version[0]}.{python_version[1:]} "
if len(version_parts) > 1:
build_description += f"(ABI {version_parts[1]}) "
try:
build_description += PLATFORM_IDENTIFIER_DESCRIPTIONS[platform_identifier]
@@ -229,32 +399,6 @@ def build_description_from_identifier(identifier: str) -> str:
return build_description
class Colors:
def __init__(self, *, enabled: bool) -> None:
self.red = "\033[31m" if enabled else ""
self.green = "\033[32m" if enabled else ""
self.yellow = "\033[33m" if enabled else ""
self.blue = "\033[34m" if enabled else ""
self.cyan = "\033[36m" if enabled else ""
self.bright_red = "\033[91m" if enabled else ""
self.bright_green = "\033[92m" if enabled else ""
self.white = "\033[37m\033[97m" if enabled else ""
self.gray = "\033[38;5;244m" if enabled else ""
self.bg_grey = "\033[48;5;235m" if enabled else ""
self.bold = "\033[1m" if enabled else ""
self.faint = "\033[2m" if enabled else ""
self.end = "\033[0m" if enabled else ""
class Symbols:
def __init__(self, *, unicode: bool) -> None:
self.done = "" if unicode else "done"
self.error = "" if unicode else "failed"
def file_supports_color(file_obj: IO[AnyStr]) -> bool:
"""
Returns True if the running system's terminal supports color.
+53 -57
View File
@@ -1,5 +1,4 @@
from __future__ import annotations
import dataclasses
import io
import json
import os
@@ -12,24 +11,17 @@ import textwrap
import typing
import uuid
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path, PurePath, PurePosixPath
from types import TracebackType
from typing import IO, Dict, Literal
from typing import IO, Literal, Self, assert_never
from ._compat.typing import Self, assert_never
from .ci import CIProvider, detect_ci_provider
from .errors import OCIEngineTooOldError
from .logger import log
from .typing import PathOrStr, PopenBytes
from .util import (
CIProvider,
FlexibleVersion,
call,
detect_ci_provider,
parse_key_value_string,
strtobool,
)
from .typing import PathOrStr
from .util.cmd import call
from .util.helpers import FlexibleVersion, parse_key_value_string, strtobool
ContainerEngineName = Literal["docker", "podman"]
@@ -41,17 +33,19 @@ class OCIPlatform(Enum):
ARMV7 = "linux/arm/v7"
ARM64 = "linux/arm64"
PPC64LE = "linux/ppc64le"
RISCV64 = "linux/riscv64"
S390X = "linux/s390x"
@dataclass(frozen=True)
@dataclasses.dataclass(frozen=True)
class OCIContainerEngineConfig:
name: ContainerEngineName
create_args: tuple[str, ...] = field(default_factory=tuple)
_: dataclasses.KW_ONLY
create_args: tuple[str, ...] = dataclasses.field(default_factory=tuple)
disable_host_mount: bool = False
@staticmethod
def from_config_string(config_string: str) -> OCIContainerEngineConfig:
@classmethod
def from_config_string(cls, config_string: str) -> Self:
config_dict = parse_key_value_string(
config_string,
["name"],
@@ -81,9 +75,7 @@ class OCIContainerEngineConfig:
else:
create_args = [arg for arg in create_args if not arg.startswith("--platform=")]
return OCIContainerEngineConfig(
name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount
)
return cls(name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount)
def options_summary(self) -> str | dict[str, str]:
if not self.create_args:
@@ -103,43 +95,46 @@ def _check_engine_version(engine: OCIContainerEngineConfig) -> None:
try:
version_string = call(engine.name, "version", "-f", "{{json .}}", capture_stdout=True)
version_info = json.loads(version_string.strip())
if engine.name == "docker":
client_api_version = FlexibleVersion(version_info["Client"]["ApiVersion"])
server_api_version = FlexibleVersion(version_info["Server"]["ApiVersion"])
# --platform support was introduced in 1.32 as experimental, 1.41 removed the experimental flag
version = min(client_api_version, server_api_version)
minimum_version = FlexibleVersion("1.41")
minimum_version_str = "20.10.0" # docker version
error_msg = textwrap.dedent(
f"""
Build failed because {engine.name} is too old.
match engine.name:
case "docker":
client_api_version = FlexibleVersion(version_info["Client"]["ApiVersion"])
server_api_version = FlexibleVersion(version_info["Server"]["ApiVersion"])
# --platform support was introduced in 1.32 as experimental, 1.41 removed the experimental flag
version = min(client_api_version, server_api_version)
minimum_version = FlexibleVersion("1.41")
minimum_version_str = "20.10.0" # docker version
error_msg = textwrap.dedent(
f"""
Build failed because {engine.name} is too old.
cibuildwheel requires {engine.name}>={minimum_version_str} running API version {minimum_version}.
The API version found by cibuildwheel is {version}.
"""
)
elif engine.name == "podman":
# podman uses the same version string for "Version" & "ApiVersion"
client_version = FlexibleVersion(version_info["Client"]["Version"])
if "Server" in version_info:
server_version = FlexibleVersion(version_info["Server"]["Version"])
else:
server_version = client_version
# --platform support was introduced in v3
version = min(client_version, server_version)
minimum_version = FlexibleVersion("3")
error_msg = textwrap.dedent(
f"""
Build failed because {engine.name} is too old.
cibuildwheel requires {engine.name}>={minimum_version_str} running API version {minimum_version}.
The API version found by cibuildwheel is {version}.
"""
)
case "podman":
# podman uses the same version string for "Version" & "ApiVersion"
client_version = FlexibleVersion(version_info["Client"]["Version"])
if "Server" in version_info:
server_version = FlexibleVersion(version_info["Server"]["Version"])
else:
server_version = client_version
# --platform support was introduced in v3
version = min(client_version, server_version)
minimum_version = FlexibleVersion("3")
error_msg = textwrap.dedent(
f"""
Build failed because {engine.name} is too old.
cibuildwheel requires {engine.name}>={minimum_version}.
The version found by cibuildwheel is {version}.
"""
)
case _:
assert_never(engine.name)
cibuildwheel requires {engine.name}>={minimum_version}.
The version found by cibuildwheel is {version}.
"""
)
else:
assert_never(engine.name)
if version < minimum_version:
raise OCIEngineTooOldError(error_msg) from None
except (subprocess.CalledProcessError, KeyError, ValueError) as e:
msg = f"Build failed because {engine.name} is too old or is not working properly."
raise OCIEngineTooOldError(msg) from e
@@ -170,7 +165,7 @@ class OCIContainer:
UTILITY_PYTHON = "/opt/python/cp39-cp39/bin/python"
process: PopenBytes
process: subprocess.Popen[bytes]
bash_stdin: IO[bytes]
bash_stdout: IO[bytes]
@@ -199,7 +194,7 @@ class OCIContainer:
if oci_platform is None:
oci_platform = self.oci_platform
# we need '--pull=always' otherwise some images with the wrong platform get re-used (e.g. 386 image for amd64)
# we need '--pull=always' otherwise some images with the wrong platform get reused (e.g. 386 image for amd64)
# c.f. https://github.com/moby/moby/issues/48197#issuecomment-2282802313
pull = "always"
try:
@@ -351,6 +346,7 @@ class OCIContainer:
)
else:
exec_process: subprocess.Popen[bytes]
self.call(["mkdir", "-p", to_path.parent])
with subprocess.Popen(
[
self.engine.name,
@@ -489,7 +485,7 @@ class OCIContainer:
capture_output=True,
)
)
return typing.cast(Dict[str, str], env)
return typing.cast(dict[str, str], env)
def environment_executor(self, command: Sequence[str], environment: dict[str, str]) -> str:
# used as an EnvironmentExecutor to evaluate commands and capture output
+205 -179
View File
@@ -1,5 +1,3 @@
from __future__ import annotations
import collections
import configparser
import contextlib
@@ -9,39 +7,51 @@ import enum
import functools
import shlex
import textwrap
from collections.abc import Generator, Iterable, Set
import tomllib
from collections.abc import Callable, Generator, Iterable, Mapping, Sequence, Set
from pathlib import Path
from typing import Any, Literal, Mapping, Sequence, Union # noqa: TID251
from typing import Any, Final, Literal, Self, assert_never
from packaging.specifiers import SpecifierSet
from . import errors
from ._compat import tomllib
from ._compat.typing import assert_never
from .architecture import Architecture
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
from .frontend import BuildFrontendConfig
from .logger import log
from .oci_container import OCIContainerEngineConfig
from .projectfiles import get_requires_python_str, resolve_dependency_groups
from .selector import BuildSelector, EnableGroup, TestSelector, selector_matches
from .typing import PLATFORMS, PlatformName
from .util import (
MANYLINUX_ARCHS,
MUSLLINUX_ARCHS,
BuildFrontendConfig,
BuildSelector,
DependencyConstraints,
EnableGroups,
TestSelector,
format_safe,
read_python_configs,
resources_dir,
selector_matches,
strtobool,
unwrap,
from .util import resources
from .util.helpers import format_safe, strtobool, unwrap
from .util.packaging import DependencyConstraints
MANYLINUX_ARCHS: Final[tuple[str, ...]] = (
"x86_64",
"i686",
"pypy_x86_64",
"aarch64",
"ppc64le",
"s390x",
"armv7l",
"riscv64",
"pypy_aarch64",
"pypy_i686",
)
MUSLLINUX_ARCHS: Final[tuple[str, ...]] = (
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
"armv7l",
"riscv64",
)
@dataclasses.dataclass
@dataclasses.dataclass(kw_only=True)
class CommandLineArguments:
platform: Literal["auto", "linux", "macos", "windows"] | None
archs: str | None
@@ -51,26 +61,26 @@ class CommandLineArguments:
package_dir: Path
print_build_identifiers: bool
allow_empty: bool
prerelease_pythons: bool
debug_traceback: bool
enable: list[str]
@staticmethod
def defaults() -> CommandLineArguments:
return CommandLineArguments(
@classmethod
def defaults(cls) -> Self:
return cls(
platform="auto",
allow_empty=False,
archs=None,
only=None,
config_file="",
output_dir=Path("wheelhouse"),
package_dir=Path("."),
prerelease_pythons=False,
package_dir=Path(),
print_build_identifiers=False,
debug_traceback=False,
enable=[],
)
@dataclasses.dataclass(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class GlobalOptions:
package_dir: Path
output_dir: Path
@@ -80,25 +90,29 @@ class GlobalOptions:
allow_empty: bool
@dataclasses.dataclass(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class BuildOptions:
globals: GlobalOptions
environment: ParsedEnvironment
before_all: str
before_build: str | None
xbuild_tools: list[str] | None
repair_command: str
manylinux_images: dict[str, str] | None
musllinux_images: dict[str, str] | None
dependency_constraints: DependencyConstraints | None
dependency_constraints: DependencyConstraints
test_command: str | None
before_test: str | None
test_sources: list[str]
test_requires: list[str]
test_extras: str
test_groups: list[str]
test_environment: ParsedEnvironment
build_verbosity: int
build_frontend: BuildFrontendConfig | None
build_frontend: BuildFrontendConfig
config_settings: str
container_engine: OCIContainerEngineConfig
pyodide_version: str | None
@property
def package_dir(self) -> Path:
@@ -121,10 +135,16 @@ class BuildOptions:
return self.globals.architectures
SettingLeaf = Union[str, int, bool]
SettingLeaf = str | int | bool
SettingList = Sequence[SettingLeaf]
SettingTable = Mapping[str, Union[SettingLeaf, SettingList]]
SettingValue = Union[SettingTable, SettingList, SettingLeaf]
SettingTable = Mapping[str, SettingLeaf | SettingList]
SettingValue = SettingTable | SettingList | SettingLeaf
class InheritRule(enum.Enum):
NONE = enum.auto()
APPEND = enum.auto()
PREPEND = enum.auto()
@dataclasses.dataclass(frozen=True)
@@ -171,11 +191,12 @@ class ListFormat(OptionFormat):
A format that joins lists with a separator.
"""
def __init__(self, sep: str) -> None:
def __init__(self, sep: str, quote: Callable[[str], str] | None = None) -> None:
self.sep = sep
self.quote = quote or (lambda s: s)
def format_list(self, value: SettingList) -> str:
return self.sep.join(str(v) for v in value)
return self.sep.join(self.quote(str(v)) for v in value)
def merge_values(self, before: str, after: str) -> str:
return f"{before}{self.sep}{after}"
@@ -204,8 +225,7 @@ class ShlexTableFormat(OptionFormat):
if isinstance(v, str):
assignments.append((k, v))
elif isinstance(v, Sequence):
for inner_v in v:
assignments.append((k, str(inner_v)))
assignments.extend((k, str(inner_v)) for inner_v in v)
else:
assignments.append((k, str(v)))
@@ -260,12 +280,6 @@ class EnvironmentFormat(OptionFormat):
return f"{before} {after}"
class InheritRule(enum.Enum):
NONE = enum.auto()
APPEND = enum.auto()
PREPEND = enum.auto()
def _resolve_cascade(
*pairs: tuple[SettingValue | None, InheritRule],
ignore_empty: bool = False,
@@ -329,40 +343,46 @@ def _apply_inherit_rule(
msg = f"Don't know how to merge {before!r} and {after!r} with {rule}"
raise OptionsReaderError(msg)
if rule == InheritRule.APPEND:
return option_format.merge_values(before, after)
if rule == InheritRule.PREPEND:
return option_format.merge_values(after, before)
assert_never(rule)
match rule:
case InheritRule.APPEND:
return option_format.merge_values(before, after)
case InheritRule.PREPEND:
return option_format.merge_values(after, before)
case _:
assert_never(rule)
def _stringify_setting(
setting: SettingValue,
option_format: OptionFormat | None,
) -> str:
if isinstance(setting, Mapping):
try:
if option_format is None:
raise OptionFormat.NotSupported
return option_format.format_table(setting)
except OptionFormat.NotSupported:
msg = f"Error converting {setting!r} to a string: this setting doesn't accept a table"
raise OptionsReaderError(msg) from None
if not isinstance(setting, str) and isinstance(setting, Sequence):
try:
if option_format is None:
raise OptionFormat.NotSupported
return option_format.format_list(setting)
except OptionFormat.NotSupported:
msg = f"Error converting {setting!r} to a string: this setting doesn't accept a list"
raise OptionsReaderError(msg) from None
if isinstance(setting, (bool, int)):
return str(setting)
return setting
match setting:
case {}:
assert isinstance(setting, Mapping) # MyPy 1.15 doesn't narrow this for us
try:
if option_format is None:
raise OptionFormat.NotSupported
return option_format.format_table(setting)
except OptionFormat.NotSupported:
msg = (
f"Error converting {setting!r} to a string: this setting doesn't accept a table"
)
raise OptionsReaderError(msg) from None
case bool() | int():
return str(setting)
case [*_]:
try:
if option_format is None:
raise OptionFormat.NotSupported
return option_format.format_list(setting)
except OptionFormat.NotSupported:
msg = (
f"Error converting {setting!r} to a string: this setting doesn't accept a list"
)
raise OptionsReaderError(msg) from None
case _:
assert isinstance(setting, str) # MyPy 1.15 doesn't narrow this for us
return setting
class OptionsReader:
@@ -395,8 +415,7 @@ class OptionsReader:
self.disallow = disallow or {}
# Open defaults.toml, loading both global and platform sections
defaults_path = resources_dir / "defaults.toml"
self.default_options, self.default_platform_options = self._load_file(defaults_path)
self.default_options, self.default_platform_options = self._load_file(resources.DEFAULTS)
# Load the project config file
config_options: dict[str, Any] = {}
@@ -567,7 +586,7 @@ class Options:
self.command_line_arguments = command_line_arguments
self.env = env
self._defaults = defaults
self._image_warnings: set[str] = set()
self._image_warnings = set[str]()
self.reader = OptionsReader(
None if defaults else self.config_file_path,
@@ -583,6 +602,10 @@ class Options:
except FileNotFoundError:
self.pyproject_toml = None
# cache the build options method so repeated calls don't need to
# resolve the options again
self.build_options = functools.cache(self._compute_build_options)
@functools.cached_property
def config_file_path(self) -> Path | None:
args = self.command_line_arguments
@@ -618,28 +641,14 @@ class Options:
enable_groups = self.reader.get(
"enable", env_plat=False, option_format=ListFormat(sep=" "), env_rule=InheritRule.APPEND
)
enable = {EnableGroups(group) for group in enable_groups.split()}
free_threaded_support = strtobool(
self.reader.get("free-threaded-support", env_plat=False, ignore_empty=True)
)
prerelease_pythons = args.prerelease_pythons or strtobool(
self.env.get("CIBW_PRERELEASE_PYTHONS", "0")
)
if free_threaded_support or prerelease_pythons:
msg = (
"free-threaded-support and prerelease-pythons should be specified by enable instead"
)
if enable:
raise OptionsReaderError(msg)
log.warning(msg)
if free_threaded_support:
enable.add(EnableGroups.CPythonFreeThreading)
if prerelease_pythons:
enable.add(EnableGroups.CPythonPrerelease)
try:
enable = {
*EnableGroup.parse_option_value(enable_groups),
*EnableGroup.parse_option_value(" ".join(args.enable)),
}
except ValueError as e:
msg = f"Failed to parse enable group. {e}. Valid group names are: {', '.join(g.value for g in EnableGroup)}"
raise errors.ConfigurationError(msg) from e
# This is not supported in tool.cibuildwheel, as it comes from a standard location.
# Passing this in as an environment variable will override pyproject.toml, setup.cfg, or setup.py
@@ -656,30 +665,16 @@ class Options:
build_config = args.only
skip_config = ""
architectures = Architecture.all_archs(self.platform)
enable = set(EnableGroups)
enable = set(EnableGroup)
build_selector = BuildSelector(
build_config=build_config,
skip_config=skip_config,
requires_python=requires_python,
enable=frozenset(
enable | {EnableGroups.PyPy}
), # For backwards compatibility, we are adding PyPy for now
enable=frozenset(enable),
)
test_selector = TestSelector(skip_config=test_skip)
all_configs = read_python_configs(self.platform)
all_pypy_ids = {
config["identifier"] for config in all_configs if config["identifier"].startswith("pp")
}
if (
not self._defaults
and EnableGroups.PyPy not in enable
and any(build_selector(build_id) for build_id in all_pypy_ids)
):
msg = "PyPy builds will be disabled by default in version 3. Enabling PyPy builds should be specified by enable"
log.warning(msg)
return GlobalOptions(
package_dir=package_dir,
output_dir=output_dir,
@@ -690,10 +685,18 @@ class Options:
)
def _check_pinned_image(self, value: str, pinned_images: Mapping[str, str]) -> None:
if (
value in {"manylinux1", "manylinux2010", "manylinux_2_24", "musllinux_1_1"}
and value not in self._image_warnings
):
error_set = {"manylinux1", "manylinux2010", "manylinux_2_24", "musllinux_1_1"}
warning_set: set[str] = set()
if value in error_set:
msg = (
f"cibuildwheel 3.x does not support the image {value!r}. Either upgrade to a "
"supported image or continue using the image by pinning it directly with"
" its full OCI registry '<name>{:<tag>|@<digest>}'."
)
raise errors.DeprecationError(msg)
if value in warning_set and value not in self._image_warnings:
self._image_warnings.add(value)
msg = (
f"Deprecated image {value!r}. This value will not work"
@@ -703,9 +706,11 @@ class Options:
)
log.warning(msg)
def build_options(self, identifier: str | None) -> BuildOptions:
def _compute_build_options(self, identifier: str | None) -> BuildOptions:
"""
Compute BuildOptions for a single run configuration.
Compute BuildOptions for a single run configuration. Normally accessed
through the `build_options` method, which is the same but the result
is cached.
"""
with self.reader.identifier(identifier):
@@ -723,9 +728,34 @@ class Options:
"config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="=")
)
dependency_versions = self.reader.get("dependency-versions")
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
xbuild_tools: list[str] | None = shlex.split(
self.reader.get(
"xbuild-tools", option_format=ListFormat(sep=" ", quote=shlex.quote)
)
)
# ["\u0000"] is a sentinel value used as a default, because TOML
# doesn't have an explicit NULL value. If xbuild-tools is set to the
# sentinel, it indicates that the user hasn't defined xbuild-tools
# *at all* (not even an `xbuild-tools = []` definition).
if xbuild_tools == ["\u0000"]:
xbuild_tools = None
test_sources = shlex.split(
self.reader.get(
"test-sources", option_format=ListFormat(sep=" ", quote=shlex.quote)
)
)
test_environment_config = self.reader.get(
"test-environment", option_format=EnvironmentFormat()
)
try:
test_environment = parse_environment(test_environment_config)
except (EnvironmentParseError, ValueError) as e:
msg = f"Malformed environment option {test_environment_config!r}"
raise errors.ConfigurationError(msg) from e
test_requires = self.reader.get(
"test-requires", option_format=ListFormat(sep=" ")
).split()
@@ -742,9 +772,8 @@ class Options:
env_plat=False,
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
)
build_frontend: BuildFrontendConfig | None
if not build_frontend_str or build_frontend_str == "default":
build_frontend = None
build_frontend = BuildFrontendConfig("build")
else:
try:
build_frontend = BuildFrontendConfig.from_config_string(build_frontend_str)
@@ -764,15 +793,18 @@ class Options:
with contextlib.suppress(KeyError):
environment.add(env_var_name, self.env[env_var_name], prepend=True)
if dependency_versions == "pinned":
dependency_constraints: None | (
DependencyConstraints
) = DependencyConstraints.with_defaults()
elif dependency_versions == "latest":
dependency_constraints = None
else:
dependency_versions_path = Path(dependency_versions)
dependency_constraints = DependencyConstraints(dependency_versions_path)
dependency_versions_str = self.reader.get(
"dependency-versions",
env_plat=True,
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
)
try:
dependency_constraints = DependencyConstraints.from_config_string(
dependency_versions_str
)
except (ValueError, OSError) as e:
msg = f"Failed to parse dependency versions. {e}"
raise errors.ConfigurationError(msg) from e
if test_extras:
test_extras = f"[{test_extras}]"
@@ -791,35 +823,26 @@ class Options:
for build_platform in MANYLINUX_ARCHS:
pinned_images = all_pinned_container_images[build_platform]
config_value = self.reader.get(
f"manylinux-{build_platform}-image", ignore_empty=True
)
if not config_value:
# default to manylinux2014
image = pinned_images["manylinux2014"]
elif config_value in pinned_images:
self._check_pinned_image(config_value, pinned_images)
self._check_pinned_image(config_value, pinned_images)
if config_value in pinned_images:
image = pinned_images[config_value]
else:
image = config_value
manylinux_images[build_platform] = image
for build_platform in MUSLLINUX_ARCHS:
pinned_images = all_pinned_container_images[build_platform]
config_value = self.reader.get(f"musllinux-{build_platform}-image")
if not config_value:
image = pinned_images["musllinux_1_2"]
elif config_value in pinned_images:
self._check_pinned_image(config_value, pinned_images)
config_value = self.reader.get(
f"musllinux-{build_platform}-image", ignore_empty=True
)
self._check_pinned_image(config_value, pinned_images)
if config_value in pinned_images:
image = pinned_images[config_value]
else:
image = config_value
musllinux_images[build_platform] = image
container_engine_str = self.reader.get(
@@ -833,9 +856,13 @@ class Options:
msg = f"Failed to parse container config. {e}"
raise errors.ConfigurationError(msg) from e
pyodide_version = self.reader.get("pyodide-version", env_plat=False)
return BuildOptions(
globals=self.globals,
test_command=test_command,
test_sources=test_sources,
test_environment=test_environment,
test_requires=[*test_requires, *test_requirements_from_groups],
test_extras=test_extras,
test_groups=test_groups,
@@ -843,6 +870,7 @@ class Options:
before_build=before_build,
before_all=before_all,
build_verbosity=build_verbosity,
xbuild_tools=xbuild_tools,
repair_command=repair_command,
environment=environment,
dependency_constraints=dependency_constraints,
@@ -851,6 +879,7 @@ class Options:
build_frontend=build_frontend,
config_settings=config_settings,
container_engine=container_engine,
pyodide_version=pyodide_version or None,
)
def check_for_invalid_configuration(self, identifiers: Iterable[str]) -> None:
@@ -868,31 +897,27 @@ class Options:
)
)
def check_for_deprecated_options(self) -> None:
build_selector = self.globals.build_selector
test_selector = self.globals.test_selector
deprecated_selectors("CIBW_BUILD", build_selector.build_config, error=True)
deprecated_selectors("CIBW_SKIP", build_selector.skip_config)
deprecated_selectors("CIBW_TEST_SKIP", test_selector.skip_config)
@functools.cached_property
def defaults(self) -> Options:
return Options(
def defaults(self) -> Self:
return self.__class__(
platform=self.platform,
command_line_arguments=CommandLineArguments.defaults(),
env={},
defaults=True,
)
def summary(self, identifiers: Iterable[str]) -> str:
def summary(self, identifiers: Iterable[str], skip_unset: bool = False) -> str:
lines = []
global_option_names = sorted(f.name for f in dataclasses.fields(self.globals))
for option_name in global_option_names:
option_value = getattr(self.globals, option_name)
default_value = getattr(self.defaults.globals, option_name)
lines.append(self.option_summary(option_name, option_value, default_value))
line = self.option_summary(
option_name, option_value, default_value, skip_unset=skip_unset
)
if line is not None:
lines.append(line)
build_options = self.build_options(identifier=None)
build_options_defaults = self.defaults.build_options(identifier=None)
@@ -912,9 +937,15 @@ class Options:
i: getattr(build_options_for_identifier[i], option_name) for i in identifiers
}
lines.append(
self.option_summary(option_name, option_value, default_value, overrides=overrides)
line = self.option_summary(
option_name,
option_value,
default_value,
overrides=overrides,
skip_unset=skip_unset,
)
if line is not None:
lines.append(line)
return "\n".join(lines)
@@ -924,7 +955,8 @@ class Options:
option_value: Any,
default_value: Any,
overrides: Mapping[str, Any] | None = None,
) -> str:
skip_unset: bool = False,
) -> str | None:
"""
Return a summary of the option value, including any overrides, with
ANSI 'dim' color if it's the default.
@@ -938,6 +970,10 @@ class Options:
overrides_value_strs = {k: v for k, v in overrides_value_strs.items() if v != value_str}
has_been_set = (value_str != default_value_str) or overrides_value_strs
if skip_unset and not has_been_set:
return None
c = log.colors
result = c.gray if not has_been_set else ""
@@ -961,13 +997,15 @@ class Options:
return result
def indent_if_multiline(self, value: str, indent: str) -> str:
@staticmethod
def indent_if_multiline(value: str, indent: str) -> str:
if "\n" in value:
return "\n" + textwrap.indent(value.strip(), indent)
else:
return value
def option_summary_value(self, option_value: Any) -> str:
@staticmethod
def option_summary_value(option_value: Any) -> str:
if hasattr(option_value, "options_summary"):
option_value = option_value.options_summary()
@@ -988,12 +1026,10 @@ def compute_options(
command_line_arguments: CommandLineArguments,
env: Mapping[str, str],
) -> Options:
options = Options(platform=platform, command_line_arguments=command_line_arguments, env=env)
options.check_for_deprecated_options()
return options
return Options(platform=platform, command_line_arguments=command_line_arguments, env=env)
@functools.lru_cache(maxsize=None)
@functools.cache
def _get_pinned_container_images() -> Mapping[str, Mapping[str, str]]:
"""
This looks like a dict of dicts, e.g.
@@ -1002,16 +1038,6 @@ def _get_pinned_container_images() -> Mapping[str, Mapping[str, str]]:
'pypy_x86_64': {'manylinux2010': '...' }
... }
"""
pinned_images_file = resources_dir / "pinned_docker_images.cfg"
all_pinned_images = configparser.ConfigParser()
all_pinned_images.read(pinned_images_file)
all_pinned_images.read(resources.PINNED_DOCKER_IMAGES)
return all_pinned_images
def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None:
if "p2" in selector or "p35" in selector:
msg = f"cibuildwheel 2.x no longer supports Python < 3.6. Please use the 1.x series or update {name}"
if error:
raise errors.DeprecationError(msg)
log.warning(msg)
+60
View File
@@ -0,0 +1,60 @@
from __future__ import annotations
import sys
from collections.abc import Sequence
from pathlib import Path
from typing import Final, Protocol
from cibuildwheel import errors
from cibuildwheel.architecture import Architecture
from cibuildwheel.options import Options
from cibuildwheel.platforms import android, ios, linux, macos, pyodide, windows
from cibuildwheel.selector import BuildSelector
from cibuildwheel.typing import GenericPythonConfiguration, PlatformName
class PlatformModule(Protocol):
# note that as per PEP544, the self argument is ignored when the protocol
# is applied to a module
def all_python_configurations(self) -> Sequence[GenericPythonConfiguration]: ...
def get_python_configurations(
self, build_selector: BuildSelector, architectures: set[Architecture]
) -> Sequence[GenericPythonConfiguration]: ...
def build(self, options: Options, tmp_path: Path) -> None: ...
ALL_PLATFORM_MODULES: Final[dict[PlatformName, PlatformModule]] = {
"linux": linux,
"windows": windows,
"macos": macos,
"pyodide": pyodide,
"android": android,
"ios": ios,
}
def native_platform() -> PlatformName:
if sys.platform.startswith("linux"):
return "linux"
elif sys.platform == "darwin":
return "macos"
elif sys.platform == "win32":
return "windows"
else:
msg = (
'Unable to detect platform from "sys.platform". cibuildwheel doesn\'t '
"support building wheels for this platform. You might be able to build for a different "
"platform using the --platform argument. Check --help output for more information."
)
raise errors.ConfigurationError(msg)
def get_build_identifiers(
platform_module: PlatformModule,
build_selector: BuildSelector,
architectures: set[Architecture],
) -> list[str]:
python_configurations = platform_module.get_python_configurations(build_selector, architectures)
return [config.identifier for config in python_configurations]
+644
View File
@@ -0,0 +1,644 @@
import csv
import hashlib
import os
import platform
import re
import shlex
import shutil
import subprocess
from collections.abc import Iterable, Iterator
from dataclasses import dataclass
from os.path import relpath
from pathlib import Path
from pprint import pprint
from runpy import run_path
from textwrap import dedent
from typing import Any
from build import ProjectBuilder
from build.env import IsolatedEnv
from elftools.common.exceptions import ELFError
from elftools.elf.elffile import ELFFile
from filelock import FileLock
from .. import errors, platforms
from ..architecture import Architecture, arch_synonym
from ..frontend import get_build_frontend_extra_flags, parse_config_settings
from ..logger import log
from ..options import BuildOptions, Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file
from ..util.helpers import prepare_command
from ..util.packaging import find_compatible_wheel
from ..util.python_build_standalone import create_python_build_standalone_environment
from ..venv import constraint_flags, virtualenv
def android_triplet(identifier: str) -> str:
return {
"arm64_v8a": "aarch64-linux-android",
"x86_64": "x86_64-linux-android",
}[parse_identifier(identifier)[1]]
def parse_identifier(identifier: str) -> tuple[str, str]:
match = re.fullmatch(r"cp(\d)(\d+)-android_(.+)", identifier)
if not match:
msg = f"invalid Android identifier: '{identifier}'"
raise ValueError(msg)
major, minor, arch = match.groups()
return (f"{major}.{minor}", arch)
@dataclass(frozen=True)
class PythonConfiguration:
version: str
identifier: str
url: str
@property
def arch(self) -> str:
return parse_identifier(self.identifier)[1]
def all_python_configurations() -> list[PythonConfiguration]:
return [PythonConfiguration(**item) for item in resources.read_python_configs("android")]
def get_python_configurations(
build_selector: BuildSelector, architectures: set[Architecture]
) -> list[PythonConfiguration]:
return [
c
for c in all_python_configurations()
if c.arch in architectures and build_selector(c.identifier)
]
def shell_prepared(command: str, *, build_options: BuildOptions, env: dict[str, str]) -> None:
shell(
prepare_command(command, project=".", package=build_options.package_dir),
env=env,
)
def before_all(options: Options, python_configurations: list[PythonConfiguration]) -> None:
before_all_options = options.build_options(python_configurations[0].identifier)
if before_all_options.before_all:
log.step("Running before_all...")
shell_prepared(
before_all_options.before_all,
build_options=before_all_options,
env=before_all_options.environment.as_dictionary(os.environ),
)
@dataclass(frozen=True)
class BuildState:
config: PythonConfiguration
options: BuildOptions
build_path: Path
python_dir: Path
build_env: dict[str, str]
android_env: dict[str, str]
def build(options: Options, tmp_path: Path) -> None:
if "ANDROID_HOME" not in os.environ:
msg = (
"ANDROID_HOME environment variable is not set. For instructions, see "
"https://cibuildwheel.pypa.io/en/stable/platforms/#android"
)
raise errors.FatalError(msg)
configs = get_python_configurations(
options.globals.build_selector, options.globals.architectures
)
if not configs:
return
try:
before_all(options, configs)
built_wheels: list[Path] = []
for config in configs:
log.build_start(config.identifier)
build_options = options.build_options(config.identifier)
build_path = tmp_path / config.identifier
build_path.mkdir()
python_dir = setup_target_python(config, build_path)
build_env, android_env = setup_env(config, build_options, build_path, python_dir)
state = BuildState(
config, build_options, build_path, python_dir, build_env, android_env
)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
print(
f"\nFound previously built wheel {compatible_wheel.name} that is "
f"compatible with {config.identifier}. Skipping build step..."
)
repaired_wheel = compatible_wheel
else:
before_build(state)
built_wheel = build_wheel(state)
repaired_wheel = repair_wheel(state, built_wheel)
test_wheel(state, repaired_wheel)
output_wheel: Path | None = None
if compatible_wheel is None:
output_wheel = move_file(
repaired_wheel, build_options.output_dir / repaired_wheel.name
)
built_wheels.append(output_wheel)
shutil.rmtree(build_path)
log.build_end(output_wheel)
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
def setup_target_python(config: PythonConfiguration, build_path: Path) -> Path:
log.step("Installing target Python...")
python_tgz = CIBW_CACHE_PATH / config.url.rpartition("/")[-1]
with FileLock(f"{python_tgz}.lock"):
if not python_tgz.exists():
download(config.url, python_tgz)
python_dir = build_path / "python"
python_dir.mkdir()
shutil.unpack_archive(python_tgz, python_dir)
return python_dir
def setup_env(
config: PythonConfiguration, build_options: BuildOptions, build_path: Path, python_dir: Path
) -> tuple[dict[str, str], dict[str, str]]:
"""
Returns two environment dicts, both pointing at the same virtual environment:
* build_env, which uses the environment normally.
* android_env, which uses the environment while simulating running on Android.
"""
log.step("Setting up build environment...")
# Create virtual environment
python_exe = create_python_build_standalone_environment(
config.version, build_path, CIBW_CACHE_PATH
)
venv_dir = build_path / "venv"
dependency_constraint = build_options.dependency_constraints.get_for_python_version(
version=config.version, tmp_dir=build_path
)
build_env = virtualenv(
config.version, python_exe, venv_dir, dependency_constraint, use_uv=False
)
create_cmake_toolchain(config, build_path, python_dir, build_env)
# Apply custom environment variables, and check environment is still valid
build_env = build_options.environment.as_dictionary(build_env)
build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
for command in ["python", "pip"]:
which = call("which", command, env=build_env, capture_stdout=True).strip()
if which != f"{venv_dir}/bin/{command}":
msg = (
f"{command} available on PATH doesn't match our installed instance. If you "
f"have modified PATH, ensure that you don't overwrite cibuildwheel's entry "
f"or insert {command} above it."
)
raise errors.FatalError(msg)
call(command, "--version", env=build_env)
# Construct an altered environment which simulates running on Android.
android_env = setup_android_env(config, python_dir, venv_dir, build_env)
# Install build tools
build_frontend = build_options.build_frontend
if build_frontend.name != "build":
msg = "Android requires the build frontend to be 'build'"
raise errors.FatalError(msg)
call("pip", "install", "build", *constraint_flags(dependency_constraint), env=build_env)
# Build-time requirements must be queried within android_env, because
# `get_requires_for_build` can run arbitrary code in setup.py scripts, which may be
# affected by the target platform. However, the requirements must be installed
# within build_env, because they're going to run on the build machine.
#
# The `build` CLI doesn't support this combination, so we use its API to query the
# requirements, and then install them ourselves with pip. We'll later run `build` in
# the same environment, passing the `--no-isolation` option.
class AndroidEnv(IsolatedEnv):
@property
def python_executable(self) -> str:
return f"{venv_dir}/bin/python"
def make_extra_environ(self) -> dict[str, str]:
return android_env
pb = ProjectBuilder.from_isolated_env(AndroidEnv(), build_options.package_dir)
if pb.build_system_requires:
call("pip", "install", *pb.build_system_requires, env=build_env)
requires_for_build = pb.get_requires_for_build(
"wheel", parse_config_settings(build_options.config_settings)
)
if requires_for_build:
call("pip", "install", *requires_for_build, env=build_env)
return build_env, android_env
def create_cmake_toolchain(
config: PythonConfiguration, build_path: Path, python_dir: Path, build_env: dict[str, str]
) -> None:
toolchain_path = build_path / "toolchain.cmake"
build_env["CMAKE_TOOLCHAIN_FILE"] = str(toolchain_path)
with open(toolchain_path, "w", encoding="UTF-8") as toolchain_file:
print(
dedent(
f"""\
# To support as many build systems as possible, we use environment
# variables as the single source of truth for compiler flags and paths,
# so they don't need to be specified here.
set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_SYSTEM_PROCESSOR {android_triplet(config.identifier).split("-")[0]})
# Inhibit all of CMake's own NDK handling code.
set(CMAKE_SYSTEM_VERSION 1)
# Tell CMake where to look for headers and libraries.
list(INSERT CMAKE_FIND_ROOT_PATH 0 {python_dir}/prefix)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
"""
),
file=toolchain_file,
)
def localize_sysconfigdata(
python_dir: Path, build_env: dict[str, str], sysconfigdata_path: Path
) -> dict[str, Any]:
sysconfigdata: dict[str, Any] = run_path(str(sysconfigdata_path))["build_time_vars"]
with sysconfigdata_path.open("w", encoding="UTF-8") as f:
f.write("# Generated by cibuildwheel\n")
f.write("build_time_vars = ")
sysconfigdata = localized_vars(build_env, sysconfigdata, python_dir / "prefix")
pprint(sysconfigdata, stream=f, compact=True)
return sysconfigdata
def localized_vars(
build_env: dict[str, str], orig_vars: dict[str, Any], prefix: Path
) -> dict[str, Any]:
orig_prefix = orig_vars["prefix"]
localized_vars_ = {}
for key, value in orig_vars.items():
# The host's sysconfigdata will include references to build-time paths.
# Update these to refer to the current prefix.
final = value
if isinstance(final, str):
final = final.replace(orig_prefix, str(prefix))
if key == "ANDROID_API_LEVEL":
if api_level := build_env.get(key):
final = int(api_level)
# Build systems vary in whether FLAGS variables are read from sysconfig, and if so,
# whether they're replaced by environment variables or combined with them. Even
# setuptools has changed its behavior here
# (https://github.com/pypa/setuptools/issues/4836).
#
# Ensure consistency by clearing the sysconfig variables and letting the environment
# variables take effect alone. This will also work for any non-Python build systems
# which the build script may call.
elif key in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]:
final = ""
# These variables contain an embedded copy of LDFLAGS.
elif key in ["LDSHARED", "LDCXXSHARED"]:
final = final.removesuffix(" " + orig_vars["LDFLAGS"])
localized_vars_[key] = final
return localized_vars_
def setup_android_env(
config: PythonConfiguration, python_dir: Path, venv_dir: Path, build_env: dict[str, str]
) -> dict[str, str]:
site_packages = next(venv_dir.glob("lib/python*/site-packages"))
for suffix in ["pth", "py"]:
shutil.copy(resources.PATH / f"_cross_venv.{suffix}", site_packages)
sysconfigdata_path = Path(
shutil.copy(
next(python_dir.glob("prefix/lib/python*/_sysconfigdata_*.py")),
site_packages,
)
)
sysconfigdata = localize_sysconfigdata(python_dir, build_env, sysconfigdata_path)
# Activate the code in _cross_venv.py.
android_env = build_env.copy()
android_env["CIBW_HOST_TRIPLET"] = android_triplet(config.identifier)
# Get the environment variables needed to build for Android (CC, CFLAGS, etc). These are
# generated by https://github.com/python/cpython/blob/main/Android/android-env.sh.
env_output = call(python_dir / "android.py", "env", env=build_env, capture_stdout=True)
# shlex.split should produce a sequence alternating between:
# * the word "export"
# * a key=value string, without quotes
for i, token in enumerate(shlex.split(env_output)):
if i % 2 == 0:
assert token == "export", token
else:
key, sep, value = token.partition("=")
assert sep == "=", token
android_env[key] = value
# localized_vars cleared the CFLAGS and CXXFLAGS in the sysconfigdata, but most
# packages take their optimization flags from these variables. Pass these flags via
# environment variables instead.
#
# We don't enable debug information, because it significantly increases binary size,
# and most Android app developers don't have the NDK installed, so they would have no
# way to strip it.
opt = " ".join(word for word in sysconfigdata["OPT"].split() if not word.startswith("-g"))
for key in ["CFLAGS", "CXXFLAGS"]:
android_env[key] += " " + opt
# Format the environment so it can be pasted into a shell when debugging.
for key, value in sorted(android_env.items()):
if os.environ.get(key) != value:
print(f"export {key}={shlex.quote(value)}")
return android_env
def before_build(state: BuildState) -> None:
if state.options.before_build:
log.step("Running before_build...")
shell_prepared(
state.options.before_build,
build_options=state.options,
env=state.build_env,
)
def build_wheel(state: BuildState) -> Path:
log.step("Building wheel...")
built_wheel_dir = state.build_path / "built_wheel"
call(
"python",
"-m",
"build",
state.options.package_dir,
"--wheel",
"--no-isolation",
"--skip-dependency-check",
f"--outdir={built_wheel_dir}",
*get_build_frontend_extra_flags(
state.options.build_frontend,
state.options.build_verbosity,
state.options.config_settings,
),
env=state.android_env,
)
built_wheels = list(built_wheel_dir.glob("*.whl"))
if len(built_wheels) != 1:
msg = f"{built_wheel_dir} contains {len(built_wheels)} wheels; expected 1"
raise errors.FatalError(msg)
built_wheel = built_wheels[0]
if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
return built_wheel
def repair_wheel(state: BuildState, built_wheel: Path) -> Path:
log.step("Repairing wheel...")
repaired_wheel_dir = state.build_path / "repaired_wheel"
repaired_wheel_dir.mkdir()
if state.options.repair_command:
shell(
prepare_command(
state.options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
),
env=state.build_env,
)
else:
repair_default(state.android_env, built_wheel, repaired_wheel_dir)
repaired_wheels = list(repaired_wheel_dir.glob("*.whl"))
if len(repaired_wheels) == 0:
raise errors.RepairStepProducedNoWheelError()
if len(repaired_wheels) != 1:
msg = f"{repaired_wheel_dir} contains {len(repaired_wheels)} wheels; expected 1"
raise errors.FatalError(msg)
repaired_wheel = repaired_wheels[0]
if repaired_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
return repaired_wheel
def repair_default(
android_env: dict[str, str], built_wheel: Path, repaired_wheel_dir: Path
) -> None:
"""
Adds libc++ to the wheel if anything links against it. In the future this should be
moved to auditwheel and generalized to support more libraries.
"""
if (match := re.search(r"^(.+?)-", built_wheel.name)) is None:
msg = f"Failed to parse wheel filename: {built_wheel.name}"
raise errors.FatalError(msg)
wheel_name = match[1]
unpacked_dir = repaired_wheel_dir / "unpacked"
unpacked_dir.mkdir()
shutil.unpack_archive(built_wheel, unpacked_dir, format="zip")
# Some build systems are inconsistent about name normalization, so don't assume the
# dist-info name is identical to the wheel name.
record_paths = list(unpacked_dir.glob("*.dist-info/RECORD"))
if len(record_paths) != 1:
msg = f"{built_wheel.name} contains {len(record_paths)} dist-info/RECORD files; expected 1"
raise errors.FatalError(msg)
old_soname = "libc++_shared.so"
paths_to_patch = []
for path, elffile in elf_file_filter(
unpacked_dir / filename
for filename, *_ in csv.reader(record_paths[0].read_text().splitlines())
):
if (dynamic := elffile.get_section_by_name(".dynamic")) and any( # type: ignore[no-untyped-call]
tag.entry.d_tag == "DT_NEEDED" and tag.needed == old_soname
for tag in dynamic.iter_tags()
):
paths_to_patch.append(path)
if not paths_to_patch:
shutil.copyfile(built_wheel, repaired_wheel_dir / built_wheel.name)
else:
# Android doesn't support DT_RPATH, but supports DT_RUNPATH since API level 24
# (https://github.com/aosp-mirror/platform_bionic/blob/master/android-changes-for-ndk-developers.md).
if int(sysconfig_print('get_config_vars()["ANDROID_API_LEVEL"]', android_env)) < 24:
msg = f"Adding {old_soname} requires ANDROID_API_LEVEL to be at least 24"
raise errors.FatalError(msg)
toolchain = Path(android_env["CC"]).parent.parent
src_path = toolchain / f"sysroot/usr/lib/{android_env['CIBW_HOST_TRIPLET']}/{old_soname}"
# Use the same library location as auditwheel would.
libs_dir = unpacked_dir / (wheel_name + ".libs")
libs_dir.mkdir()
new_soname = soname_with_hash(src_path)
dst_path = libs_dir / new_soname
shutil.copyfile(src_path, dst_path)
call("patchelf", "--set-soname", new_soname, dst_path)
for path in paths_to_patch:
call("patchelf", "--replace-needed", old_soname, new_soname, path)
call(
"patchelf",
"--set-rpath",
f"${{ORIGIN}}/{relpath(libs_dir, path.parent)}",
path,
)
call("wheel", "pack", unpacked_dir, "-d", repaired_wheel_dir)
def elf_file_filter(paths: Iterable[Path]) -> Iterator[tuple[Path, ELFFile]]:
"""Filter through an iterator of filenames and load up only ELF files"""
for path in paths:
if not path.name.endswith(".py"):
try:
with open(path, "rb") as f:
candidate = ELFFile(f) # type: ignore[no-untyped-call]
yield path, candidate
except ELFError:
pass # Not an ELF file
def soname_with_hash(src_path: Path) -> str:
"""Return the same library filename as auditwheel would"""
shorthash = hashlib.sha256(src_path.read_bytes()).hexdigest()[:8]
src_name = src_path.name
base, ext = src_name.split(".", 1)
if not base.endswith(f"-{shorthash}"):
return f"{base}-{shorthash}.{ext}"
else:
return src_name
def test_wheel(state: BuildState, wheel: Path) -> None:
test_command = state.options.test_command
if not (test_command and state.options.test_selector(state.config.identifier)):
return
log.step("Testing wheel...")
native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android")
if state.config.arch != native_arch:
log.warning(
f"Skipping tests for {state.config.arch}, as the build machine only "
f"supports {native_arch}"
)
return
if state.options.before_test:
shell_prepared(
state.options.before_test,
build_options=state.options,
env=state.build_env,
)
# Install the wheel and test-requires.
site_packages_dir = state.build_path / "site-packages"
site_packages_dir.mkdir()
call(
"pip",
"install",
"--only-binary=:all:",
"--platform",
sysconfig_print("get_platform()", state.android_env).replace("-", "_"),
"--target",
site_packages_dir,
f"{wheel}{state.options.test_extras}",
*state.options.test_requires,
env=state.build_env,
)
# Copy test-sources.
cwd_dir = state.build_path / "cwd"
cwd_dir.mkdir()
if state.options.test_sources:
copy_test_sources(state.options.test_sources, Path.cwd(), cwd_dir)
else:
(cwd_dir / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text(),
)
# Android doesn't support placeholders in the test command.
if any(("{" + placeholder + "}") in test_command for placeholder in ["project", "package"]):
msg = (
f"Test command {test_command!r} with a "
"'{project}' or '{package}' placeholder is not supported on Android, "
"because the source directory is not visible on the emulator."
)
raise errors.FatalError(msg)
# Parse test-command.
test_args = shlex.split(test_command)
if test_args[:2] in [["python", "-c"], ["python", "-m"]]:
test_args[:3] = [test_args[1], test_args[2], "--"]
elif test_args[0] in ["pytest"]:
# We transform some commands into the `python -m` form, but this is deprecated.
msg = (
f"Test command {test_command!r} is not supported on Android. "
"cibuildwheel will try to execute it as if it started with 'python -m'. "
"If this works, all you need to do is add that to your test command."
)
log.warning(msg)
test_args[:1] = ["-m", test_args[0], "--"]
else:
msg = (
f"Test command {test_command!r} is not supported on Android. "
f"Supported commands are 'python -m' and 'python -c'."
)
raise errors.FatalError(msg)
# Run the test app.
call(
state.python_dir / "android.py",
"test",
"--managed",
"maxVersion",
"--site-packages",
site_packages_dir,
"--cwd",
cwd_dir,
*test_args,
env=state.build_env,
)
def sysconfig_print(method_call: str, env: dict[str, str]) -> str:
return call(
"python",
"-c",
f'import sysconfig; print(sysconfig.{method_call}, end="")',
env=env,
capture_stdout=True,
)
+691
View File
@@ -0,0 +1,691 @@
from __future__ import annotations
import dataclasses
import os
import shlex
import shutil
import subprocess
import sys
import textwrap
from collections.abc import Sequence, Set
from pathlib import Path
from typing import assert_never
from filelock import FileLock
from .. import errors
from ..architecture import Architecture
from ..environment import ParsedEnvironment
from ..frontend import (
BuildFrontendName,
get_build_frontend_extra_flags,
)
from ..logger import log
from ..options import Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell, split_command
from ..util.file import (
CIBW_CACHE_PATH,
copy_test_sources,
download,
move_file,
)
from ..util.helpers import prepare_command, unwrap_preserving_paragraphs
from ..util.packaging import (
combine_constraints,
find_compatible_wheel,
get_pip_version,
)
from ..venv import constraint_flags, virtualenv
from .macos import install_cpython as install_build_cpython
@dataclasses.dataclass(frozen=True, kw_only=True)
class PythonConfiguration:
version: str
identifier: str
url: str
build_url: str
@property
def sdk(self) -> str:
return self.multiarch.rsplit("-", 1)[1]
@property
def arch(self) -> str:
return self.multiarch.rsplit("-", 1)[0]
@property
def multiarch(self) -> str:
# The multiarch identifier, as reported by `sys.implementation._multiarch`
return "-".join(self.identifier.split("-ios_")[1].rsplit("_", 1))
@property
def is_simulator(self) -> bool:
return self.identifier.endswith("_iphonesimulator")
@property
def xcframework_slice(self) -> str:
"XCframeworks include binaries for multiple ABIs; which ABI section should be used?"
return "ios-arm64_x86_64-simulator" if self.is_simulator else "ios-arm64"
def all_python_configurations() -> list[PythonConfiguration]:
# iOS builds are always cross builds; we need to install a macOS Python as
# well. Rather than duplicate the location of the URL of macOS installers,
# load the macos configurations, determine the macOS configuration that
# matches the platform we're building, and embed that URL in the parsed iOS
# configuration.
macos_python_configs = resources.read_python_configs("macos")
def build_url(config_dict: dict[str, str]) -> str:
# The iOS identifier will be something like cp313-ios_arm64_iphoneos.
# Drop the iphoneos suffix, then replace ios with macosx to yield
# cp313-macosx_arm64, which will be a macOS build identifier.
modified_ios_identifier = config_dict["identifier"].rsplit("_", 1)[0]
macos_identifier = modified_ios_identifier.replace("ios", "macosx")
matching = [
config for config in macos_python_configs if config["identifier"] == macos_identifier
]
return matching[0]["url"]
# Load the platform configuration
full_python_configs = resources.read_python_configs("ios")
# Build the configurations, annotating with macOS URL details.
return [
PythonConfiguration(
**item,
build_url=build_url(item),
)
for item in full_python_configs
]
def get_python_configurations(
build_selector: BuildSelector,
architectures: Set[Architecture],
) -> list[PythonConfiguration]:
python_configurations = all_python_configurations()
# Filter out configs that don't match any of the selected architectures
python_configurations = [
c
for c in python_configurations
if any(c.identifier.endswith(f"-ios_{a.value}") for a in architectures)
]
# Skip builds as required by BUILD/SKIP
python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
return python_configurations
def install_target_cpython(tmp: Path, config: PythonConfiguration, free_threading: bool) -> Path:
if free_threading:
msg = "Free threading builds aren't available for iOS (yet)"
raise errors.FatalError(msg)
# Install an iOS build of CPython
ios_python_tar_gz = config.url.rsplit("/", 1)[-1]
extension = ".tar.gz"
assert ios_python_tar_gz.endswith(extension)
installation_path = CIBW_CACHE_PATH / ios_python_tar_gz[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
downloaded_tar_gz = tmp / ios_python_tar_gz
download(config.url, downloaded_tar_gz)
installation_path.mkdir(parents=True, exist_ok=True)
call("tar", "-C", installation_path, "-xf", downloaded_tar_gz)
downloaded_tar_gz.unlink()
return installation_path
def cross_virtualenv(
*,
py_version: str,
target_python: Path,
multiarch: str,
build_python: Path,
venv_path: Path,
dependency_constraint: Path | None,
xbuild_tools: Sequence[str] | None,
) -> dict[str, str]:
"""Create a cross-compilation virtual environment.
In a cross-compilation environment, the *target* is the platform where the
code will ultimately run, and the *build* is the platform where you're
running the compilation. When building iOS wheels, iOS is the target machine
and macOS is the build machine. The terminology around these machines varies
between build tools (configure uses "host" and "build"; cmake uses "target" and
"build host").
A cross-compilation virtualenv is an environment that is based on the
*build* python (so that binaries can execute); but it modifies the
environment at startup so that any request for platform details (such as
`sys.platform` or `sysconfig.get_platform()`) return details of the target
platform. It also applies a loader patch so that any virtualenv created by
the cross-compilation environment will also be a cross-compilation
environment.
:param py_version: The Python version (major.minor) in use
:param target_python: The path to the python binary for the target platform
:param multiarch: The multiarch tag for the target platform (i.e., the value
of `sys.implementation._multiarch`)
:param build_python: The path to the python binary for the build platform
:param venv_path: The path where the cross virtual environment should be
created.
:param dependency_constraint: A path to a constraint file that should be
used when constraining dependencies in the environment.
:param xbuild_tools: A list of executable names (without paths) that are
on the path, but must be preserved in the cross environment.
"""
# Create an initial macOS virtual environment
env = virtualenv(
py_version,
build_python,
venv_path,
dependency_constraint,
use_uv=False,
)
# Convert the macOS virtual environment into an iOS virtual environment
# using the cross-platform conversion script in the iOS distribution.
# target_python is the path to the Python binary;
# determine the root of the XCframework slice that is being used.
slice_path = target_python.parent.parent
call(
"python",
str(slice_path / f"platform-config/{multiarch}/make_cross_venv.py"),
str(venv_path),
env=env,
cwd=venv_path,
)
# When running on macOS, it's easy for the build environment to leak into
# the target environment, especially when building for ARM64 (because the
# build architecture is the same as the target architecture). The primary
# culprit for this is Homebrew libraries leaking in as dependencies for iOS
# libraries.
#
# To prevent problems, set the PATH to isolate the build environment from
# sources that could introduce incompatible binaries.
#
# However, there may be some tools on the path that are needed for the
# build. Find their location on the path, and link the underlying binaries
# (fully resolving symlinks) to a "safe" location that will *only* contain
# those tools. This avoids needing to add *all* of Homebrew to the path just
# to get access to (for example) cmake for build purposes. A value of None
# means the user hasn't provided a list of xbuild tools.
xbuild_tools_path = venv_path / "cibw_xbuild_tools"
xbuild_tools_path.mkdir()
if xbuild_tools is None:
log.warning(
textwrap.dedent(
"""
Your project configuration does not define any cross-build tools.
iOS builds use an isolated build environment; if your build process requires any
third-party tools (such as cmake, ninja, or rustc), you must explicitly declare
that those tools are required using xbuild-tools/CIBW_XBUILD_TOOLS. This will
likely manifest as a "somebuildtool: command not found" error.
If the build succeeds, you can silence this warning by setting adding
`xbuild-tools = []` to your pyproject.toml configuration, or exporting
CIBW_XBUILD_TOOLS as an empty string into your environment.
"""
)
)
else:
for tool in xbuild_tools:
tool_path = shutil.which(tool)
if tool_path is None:
msg = f"Could not find a {tool!r} executable on the path."
raise errors.FatalError(msg)
# Link the binary into the safe tools directory
original = Path(tool_path).resolve()
print(f"{tool!r} will be included in the cross-build environment (using {original})")
(xbuild_tools_path / tool).symlink_to(original)
env["PATH"] = os.pathsep.join(
[
# The target python's binary directory
str(target_python.parent),
# The cross-platform environment's binary directory
str(venv_path / "bin"),
# The directory of cross-build tools
str(xbuild_tools_path),
# The bare minimum Apple system paths.
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin",
"/Library/Apple/usr/bin",
]
)
# Also unset DYLD_LIBRARY_PATH to ensure that no macOS libraries will be
# found and linked.
env.pop("DYLD_LIBRARY_PATH", None)
return env
def setup_python(
tmp: Path,
*,
python_configuration: PythonConfiguration,
dependency_constraint: Path | None,
environment: ParsedEnvironment,
build_frontend: BuildFrontendName,
xbuild_tools: Sequence[str] | None,
) -> tuple[Path, dict[str, str]]:
if build_frontend == "build[uv]":
msg = "uv doesn't support iOS"
raise errors.FatalError(msg)
# An iOS environment requires 2 python installs - one for the build machine
# (macOS), and one for the target (iOS). We'll only ever interact with the
# *target* python, but the build Python needs to exist to act as the base
# for a cross venv.
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
log.step(f"Installing Build Python {implementation_id}...")
if implementation_id.startswith("cp"):
free_threading = "t-ios" in python_configuration.identifier
build_python = install_build_cpython(
tmp,
python_configuration.version,
python_configuration.build_url,
free_threading,
)
else:
msg = f"Unknown Python implementation: {implementation_id}"
raise errors.FatalError(msg)
assert build_python.exists(), (
f"{build_python.name} not found, has {list(build_python.parent.iterdir())}"
)
log.step(f"Installing Target Python {implementation_id}...")
target_install_path = install_target_cpython(tmp, python_configuration, free_threading)
target_python = (
target_install_path
/ "Python.xcframework"
/ python_configuration.xcframework_slice
/ "bin"
/ f"python{python_configuration.version}"
)
assert target_python.exists(), (
f"{target_python.name} not found, has {list(target_install_path.iterdir())}"
)
log.step("Creating cross build environment...")
venv_path = tmp / "venv"
env = cross_virtualenv(
py_version=python_configuration.version,
target_python=target_python,
multiarch=python_configuration.multiarch,
build_python=build_python,
venv_path=venv_path,
dependency_constraint=dependency_constraint,
xbuild_tools=xbuild_tools,
)
venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists()
# We version pip ourselves, so we don't care about pip version checking
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
# upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
pip = ["python", "-m", "pip"]
call(
*pip,
"install",
"--upgrade",
"pip",
*constraint_flags(dependency_constraint),
env=env,
cwd=venv_path,
)
# Apply our environment after pip is ready
env = environment.as_dictionary(prev_environment=env)
# Check what Python version we're on
which_python = call("which", "python", env=env, capture_stdout=True).strip()
print(which_python)
if which_python != str(venv_bin_path / "python"):
msg = (
"cibuildwheel: python available on PATH doesn't match our installed instance. "
"If you have modified PATH, ensure that you don't overwrite cibuildwheel's "
"entry or insert python above it."
)
raise errors.FatalError(msg)
call("python", "--version", env=env)
# Check what pip version we're on
assert (venv_bin_path / "pip").exists()
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
print(which_pip)
if which_pip != str(venv_bin_path / "pip"):
msg = (
"cibuildwheel: pip available on PATH doesn't match our installed instance. "
"If you have modified PATH, ensure that you don't overwrite cibuildwheel's "
"entry or insert pip above it."
)
raise errors.FatalError(msg)
call("pip", "--version", env=env)
# Ensure that IPHONEOS_DEPLOYMENT_TARGET is set in the environment
env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
log.step("Installing build tools...")
match build_frontend:
case "pip":
# No additional build tools required
pass
case "build":
call(
"pip",
"install",
"--upgrade",
"build[virtualenv]",
*constraint_flags(dependency_constraint),
env=env,
)
case _:
assert_never(build_frontend)
return target_install_path, env
def build(options: Options, tmp_path: Path) -> None:
if sys.platform != "darwin":
msg = "iOS binaries can only be built on macOS"
raise errors.FatalError(msg)
python_configurations = get_python_configurations(
build_selector=options.globals.build_selector,
architectures=options.globals.architectures,
)
if not python_configurations:
return
try:
before_all_options_identifier = python_configurations[0].identifier
before_all_options = options.build_options(before_all_options_identifier)
if before_all_options.before_all:
log.step("Running before_all...")
env = before_all_options.environment.as_dictionary(prev_environment=os.environ)
env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
before_all_prepared = prepare_command(
before_all_options.before_all,
project=".",
package=before_all_options.package_dir,
)
shell(before_all_prepared, env=env)
built_wheels: list[Path] = []
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend
# uv doesn't support iOS
if build_frontend.name == "build[uv]":
msg = "uv doesn't support iOS"
raise errors.FatalError(msg)
log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier
identifier_tmp_dir.mkdir()
built_wheel_dir = identifier_tmp_dir / "built_wheel"
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, tmp_dir=identifier_tmp_dir
)
target_install_path, env = setup_python(
identifier_tmp_dir / "build",
python_configuration=config,
dependency_constraint=constraints_path,
environment=build_options.environment,
build_frontend=build_frontend.name,
xbuild_tools=build_options.xbuild_tools,
)
pip_version = get_pip_version(env)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
log.step_end()
print(
f"\nFound previously built wheel {compatible_wheel.name} "
f"that is compatible with {config.identifier}. "
"Skipping build step..."
)
test_wheel = compatible_wheel
else:
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build,
project=".",
package=build_options.package_dir,
)
shell(before_build_prepared, env=env)
log.step("Building wheel...")
built_wheel_dir.mkdir()
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
)
build_env = env.copy()
build_env["VIRTUALENV_PIP"] = pip_version
if constraints_path:
combine_constraints(build_env, constraints_path, None)
match build_frontend.name:
case "pip":
# Path.resolve() is needed. Without it pip wheel may try to
# fetch package from pypi.org. See
# https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
build_options.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
env=build_env,
)
case "build":
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
case _:
assert_never(build_frontend)
test_wheel = built_wheel = next(built_wheel_dir.glob("*.whl"))
if built_wheel.name.endswith("none-any.whl"):
raise errors.NonPlatformWheelError()
log.step_end()
if build_options.test_command and build_options.test_selector(config.identifier):
if not config.is_simulator:
log.step("Skipping tests on non-simulator SDK")
elif config.arch != os.uname().machine:
log.step("Skipping tests on non-native simulator architecture")
else:
test_env = build_options.test_environment.as_dictionary(
prev_environment=build_env
)
if build_options.before_test:
before_test_prepared = prepare_command(
build_options.before_test,
project=".",
package=build_options.package_dir,
)
shell(before_test_prepared, env=test_env)
log.step("Setting up test harness...")
# Clone the testbed project into the build directory
testbed_path = identifier_tmp_dir / "testbed"
call(
"python",
target_install_path / "testbed",
"clone",
testbed_path,
env=test_env,
)
testbed_app_path = testbed_path / "iOSTestbed" / "app"
# Copy the test sources to the testbed app
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
Path.cwd(),
testbed_app_path,
)
else:
(testbed_app_path / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text()
)
log.step("Installing test requirements...")
# Install the compiled wheel (with any test extras), plus
# the test requirements. Use the --platform tag to force
# the installation of iOS wheels; this requires the use of
# --only-binary=:all:
ios_version = test_env["IPHONEOS_DEPLOYMENT_TARGET"]
platform_tag = f"ios_{ios_version.replace('.', '_')}_{config.arch}_{config.sdk}"
call(
"python",
"-m",
"pip",
"install",
"--only-binary=:all:",
"--platform",
platform_tag,
"--target",
testbed_path / "iOSTestbed" / "app_packages",
f"{test_wheel}{build_options.test_extras}",
*build_options.test_requires,
env=test_env,
)
log.step("Running test suite...")
# iOS doesn't support placeholders in the test command,
# because the source dir isn't visible on the simulator.
if (
"{project}" in build_options.test_command
or "{package}" in build_options.test_command
):
msg = unwrap_preserving_paragraphs(
f"""
iOS tests configured with a test command that uses the "{{project}}" or
"{{package}}" placeholder. iOS tests cannot use placeholders, because the
source directory is not visible on the simulator.
In addition, iOS tests must run as a Python module, so the test command
must begin with 'python -m'.
Test command: {build_options.test_command!r}
"""
)
raise errors.FatalError(msg)
test_command_list = shlex.split(build_options.test_command)
try:
for test_command_parts in split_command(test_command_list):
match test_command_parts:
case ["python", "-m", *rest]:
final_command = rest
case ["pytest", *rest]:
# pytest works exactly the same as a module, so we
# can just run it as a module.
msg = unwrap_preserving_paragraphs(f"""
iOS tests configured with a test command which doesn't start
with 'python -m'. iOS tests must execute python modules - other
entrypoints are not supported.
cibuildwheel will try to execute it as if it started with
'python -m'. If this works, all you need to do is add that to
your test command.
Test command: {build_options.test_command!r}
""")
log.warning(msg)
final_command = ["pytest", *rest]
case _:
msg = unwrap_preserving_paragraphs(
f"""
iOS tests configured with a test command which doesn't start
with 'python -m'. iOS tests must execute python modules - other
entrypoints are not supported.
Test command: {build_options.test_command!r}
"""
)
raise errors.FatalError(msg)
call(
"python",
testbed_path,
"run",
*(["--verbose"] if build_options.build_verbosity > 0 else []),
"--",
*final_command,
env=test_env,
)
except subprocess.CalledProcessError:
# catches the first test command failure in the loop,
# implementing short-circuiting
log.step_end(success=False)
log.error(f"Test suite failed on {config.identifier}")
sys.exit(1)
log.step_end()
# We're all done here; move it to output (overwrite existing)
output_wheel: Path | None = None
if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(built_wheel.name)
moved_wheel = move_file(built_wheel, output_wheel)
if moved_wheel != output_wheel.resolve():
log.warning(
f"{built_wheel} was moved to {moved_wheel} instead of {output_wheel}"
)
built_wheels.append(output_wheel)
# Clean up
shutil.rmtree(identifier_tmp_dir)
log.build_end(output_wheel)
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
@@ -1,34 +1,25 @@
from __future__ import annotations
import contextlib
import dataclasses
import subprocess
import sys
import textwrap
from collections import OrderedDict
from collections.abc import Iterable, Iterator, Sequence, Set
from dataclasses import dataclass
from pathlib import Path, PurePath, PurePosixPath
from typing import OrderedDict, Tuple
from typing import assert_never
from packaging.version import Version
from . import errors
from ._compat.typing import assert_never
from .architecture import Architecture
from .logger import log
from .oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
from .options import BuildOptions, Options
from .typing import PathOrStr
from .util import (
BuildFrontendConfig,
BuildSelector,
find_compatible_wheel,
get_build_verbosity_extra_flags,
prepare_command,
read_python_configs,
split_config_settings,
test_fail_cwd_file,
unwrap,
)
from .. import errors
from ..architecture import Architecture
from ..frontend import get_build_frontend_extra_flags
from ..logger import log
from ..oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
from ..options import BuildOptions, Options
from ..selector import BuildSelector
from ..typing import PathOrStr
from ..util import resources
from ..util.file import copy_test_sources
from ..util.helpers import prepare_command, unwrap
from ..util.packaging import find_compatible_wheel
ARCHITECTURE_OCI_PLATFORM_MAP = {
Architecture.x86_64: OCIPlatform.AMD64,
@@ -37,10 +28,11 @@ ARCHITECTURE_OCI_PLATFORM_MAP = {
Architecture.ppc64le: OCIPlatform.PPC64LE,
Architecture.s390x: OCIPlatform.S390X,
Architecture.armv7l: OCIPlatform.ARMV7,
Architecture.riscv64: OCIPlatform.RISCV64,
}
@dataclass(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class PythonConfiguration:
version: str
identifier: str
@@ -51,7 +43,7 @@ class PythonConfiguration:
return PurePosixPath(self.path_str)
@dataclass(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class BuildStep:
platform_configs: list[PythonConfiguration]
platform_tag: str
@@ -59,13 +51,16 @@ class BuildStep:
container_image: str
def all_python_configurations() -> list[PythonConfiguration]:
config_dicts = resources.read_python_configs("linux")
return [PythonConfiguration(**item) for item in config_dicts]
def get_python_configurations(
build_selector: BuildSelector,
architectures: Set[Architecture],
) -> list[PythonConfiguration]:
full_python_configs = read_python_configs("linux")
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
python_configurations = all_python_configurations()
# return all configurations whose arch is in our `architectures` set,
# and match the build/skip rules
@@ -104,7 +99,7 @@ def get_build_steps(
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
separate container instance.
"""
steps = OrderedDict[Tuple[str, str, str, OCIContainerEngineConfig], BuildStep]()
steps = OrderedDict[tuple[str, str, str, OCIContainerEngineConfig], BuildStep]()
for config in python_configurations:
_, platform_tag = config.identifier.split("-", 1)
@@ -173,6 +168,7 @@ def build_in_container(
container: OCIContainer,
container_project_path: PurePath,
container_package_dir: PurePath,
local_tmp_dir: Path,
) -> None:
container_output_dir = PurePosixPath("/output")
@@ -188,7 +184,7 @@ def build_in_container(
log.step("Running before_all...")
env = container.get_environment()
env["PATH"] = f'/opt/python/cp39-cp39/bin:{env["PATH"]}'
env["PATH"] = f"/opt/python/cp39-cp39/bin:{env['PATH']}"
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env["PIP_ROOT_USER_ACTION"] = "ignore"
env = before_all_options.environment.as_dictionary(
@@ -206,22 +202,22 @@ def build_in_container(
for config in platform_configs:
log.build_start(config.identifier)
local_identifier_tmp_dir = local_tmp_dir / config.identifier
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
use_uv = build_frontend.name == "build[uv]" and Version(config.version) >= Version("3.8")
build_frontend = build_options.build_frontend
use_uv = build_frontend.name == "build[uv]"
pip = ["uv", "pip"] if use_uv else ["pip"]
dependency_constraint_flags: list[PathOrStr] = []
log.step("Setting up build environment...")
if build_options.dependency_constraints:
constraints_file = build_options.dependency_constraints.get_for_python_version(
config.version
)
dependency_constraint_flags: list[PathOrStr] = []
local_constraints_file = build_options.dependency_constraints.get_for_python_version(
version=config.version,
tmp_dir=local_identifier_tmp_dir,
)
if local_constraints_file:
container_constraints_file = PurePosixPath("/constraints.txt")
container.copy_into(constraints_file, container_constraints_file)
container.copy_into(local_constraints_file, container_constraints_file)
dependency_constraint_flags = ["-c", container_constraints_file]
env = container.get_environment()
@@ -230,7 +226,7 @@ def build_in_container(
# put this config's python top of the list
python_bin = config.path / "bin"
env["PATH"] = f'{python_bin}:{env["PATH"]}'
env["PATH"] = f"{python_bin}:{env['PATH']}"
env = build_options.environment.as_dictionary(env, executor=container.environment_executor)
@@ -257,7 +253,7 @@ def build_in_container(
print(
f"\nFound previously built wheel {compatible_wheel.name}, that's compatible with {config.identifier}. Skipping build step..."
)
repaired_wheels = [compatible_wheel]
repaired_wheel = compatible_wheel
else:
if build_options.before_build:
log.step("Running before_build...")
@@ -275,44 +271,42 @@ def build_in_container(
container.call(["rm", "-rf", built_wheel_dir])
container.call(["mkdir", "-p", built_wheel_dir])
extra_flags = split_config_settings(build_options.config_settings, build_frontend.name)
extra_flags += build_frontend.args
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
)
if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
container.call(
[
"python",
"-m",
"pip",
"wheel",
container_package_dir,
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
],
env=env,
)
elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg)
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
extra_flags += ["--installer=uv"]
container.call(
[
"python",
"-m",
"build",
container_package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
],
env=env,
)
else:
assert_never(build_frontend)
match build_frontend.name:
case "pip":
container.call(
[
"python",
"-m",
"pip",
"wheel",
container_package_dir,
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
],
env=env,
)
case "build" | "build[uv]":
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
extra_flags += ["--installer=uv"]
container.call(
[
"python",
"-m",
"build",
container_package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
],
env=env,
)
case _:
assert_never(build_frontend)
built_wheel = container.glob(built_wheel_dir, "*.whl")[0]
@@ -332,14 +326,16 @@ def build_in_container(
else:
container.call(["mv", built_wheel, repaired_wheel_dir])
repaired_wheels = container.glob(repaired_wheel_dir, "*.whl")
match container.glob(repaired_wheel_dir, "*.whl"):
case []:
raise errors.RepairStepProducedNoWheelError()
case [repaired_wheel]:
pass
case too_many:
raise errors.RepairStepProducedMultipleWheelsError([p.name for p in too_many])
if not repaired_wheels:
raise errors.RepairStepProducedNoWheelError()
for repaired_wheel in repaired_wheels:
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
if repaired_wheel.name in {wheel.name for wheel in built_wheels}:
raise errors.AlreadyBuiltWheelError(repaired_wheel.name)
if build_options.test_command and build_options.test_selector(config.identifier):
log.step("Testing wheel...")
@@ -360,15 +356,17 @@ def build_in_container(
container.call(["uv", "venv", venv_dir, "--python", python_bin / "python"], env=env)
else:
# Use embedded dependencies from virtualenv to ensure determinism
venv_args = ["--no-periodic-update", "--pip=embed"]
# In Python<3.12, setuptools & wheel are installed as well
if Version(config.version) < Version("3.12"):
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
venv_args = ["--no-periodic-update", "--pip=embed", "--no-setuptools"]
if "38" in config.identifier:
venv_args.append("--no-wheel")
container.call(["python", "-m", "virtualenv", *venv_args, venv_dir], env=env)
virtualenv_env = env.copy()
virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}"
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
)
if build_options.before_test:
before_test_prepared = prepare_command(
@@ -379,14 +377,8 @@ def build_in_container(
container.call(["sh", "-c", before_test_prepared], env=virtualenv_env)
# Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel
# conforms to multiple manylinux standards. These multiple versions of the wheel are
# functionally the same, differing only in name, wheel metadata, and possibly include
# different external shared libraries. so it doesn't matter which one we run the tests on.
# Let's just pick the first one.
wheel_to_test = repaired_wheels[0]
container.call(
[*pip, "install", str(wheel_to_test) + build_options.test_extras],
[*pip, "install", str(repaired_wheel) + build_options.test_extras],
env=virtualenv_env,
)
@@ -399,26 +391,38 @@ def build_in_container(
build_options.test_command,
project=container_project_path,
package=container_package_dir,
wheel=wheel_to_test,
wheel=repaired_wheel,
)
test_cwd = testing_temp_dir / "test_cwd"
container.call(["mkdir", "-p", test_cwd])
container.copy_into(test_fail_cwd_file, test_cwd / "test_fail.py")
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
Path.cwd(),
test_cwd,
copy_into=container.copy_into,
)
else:
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
container.copy_into(resources.TEST_FAIL_CWD_FILE, test_cwd / "test_fail.py")
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
# clean up test environment
container.call(["rm", "-rf", testing_temp_dir])
# move repaired wheels to output
# move repaired wheel to output
output_wheel: Path | None = None
if compatible_wheel is None:
container.call(["mkdir", "-p", container_output_dir])
container.call(["mv", *repaired_wheels, container_output_dir])
built_wheels.extend(
container_output_dir / repaired_wheel.name for repaired_wheel in repaired_wheels
)
container.call(["mv", repaired_wheel, container_output_dir])
built_wheels.append(container_output_dir / repaired_wheel.name)
output_wheel = options.globals.output_dir / repaired_wheel.name
log.build_end()
log.build_end(output_wheel)
log.step("Copying wheels back to host...")
# copy the output back into the host
@@ -426,7 +430,7 @@ def build_in_container(
log.step_end()
def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
def build(options: Options, tmp_path: Path) -> None:
python_configurations = get_python_configurations(
options.globals.build_selector, options.globals.architectures
)
@@ -451,12 +455,12 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
except subprocess.CalledProcessError as error:
msg = unwrap(
f"""
cibuildwheel: {build_step.container_engine.name} not found. An
OCI exe like Docker or Podman is required to run Linux builds.
If you're building on Travis CI, add `services: [docker]` to
your .travis.yml. If you're building on Circle CI in Linux,
add a `setup_remote_docker` step to your .circleci/config.yml.
If you're building on Cirrus CI, use `docker_builder` task.
{build_step.container_engine.name} not found. An OCI exe like
Docker or Podman is required to run Linux builds. If you're
building on Travis CI, add `services: [docker]` to your
.travis.yml. If you're building on Circle CI in Linux, add a
`setup_remote_docker` step to your .circleci/config.yml. If
you're building on Cirrus CI, use `docker_builder` task.
"""
)
raise errors.ConfigurationError(msg) from error
@@ -480,6 +484,7 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
container=container,
container_project_path=container_project_path,
container_package_dir=container_package_dir,
local_tmp_dir=tmp_path,
)
except subprocess.CalledProcessError as error:
@@ -1,5 +1,4 @@
from __future__ import annotations
import dataclasses
import functools
import inspect
import os
@@ -9,48 +8,35 @@ import shutil
import subprocess
import sys
import typing
from collections.abc import Sequence, Set
from dataclasses import dataclass
from collections.abc import Set
from pathlib import Path
from typing import Literal, Tuple
from typing import Literal, assert_never
from filelock import FileLock
from packaging.version import Version
from . import errors
from ._compat.typing import assert_never
from .architecture import Architecture
from .environment import ParsedEnvironment
from .logger import log
from .options import Options
from .typing import PathOrStr
from .util import (
from .. import errors
from ..architecture import Architecture
from ..ci import detect_ci_provider
from ..environment import ParsedEnvironment
from ..frontend import BuildFrontendName, get_build_frontend_extra_flags
from ..logger import log
from ..options import Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.file import (
CIBW_CACHE_PATH,
BuildFrontendConfig,
BuildFrontendName,
BuildSelector,
call,
combine_constraints,
detect_ci_provider,
copy_test_sources,
download,
find_compatible_wheel,
find_uv,
free_thread_enable_313,
get_build_verbosity_extra_flags,
get_pip_version,
install_certifi_script,
move_file,
prepare_command,
read_python_configs,
shell,
split_config_settings,
test_fail_cwd_file,
unwrap,
virtualenv,
)
from ..util.helpers import prepare_command, unwrap
from ..util.packaging import combine_constraints, find_compatible_wheel, get_pip_version
from ..venv import constraint_flags, find_uv, virtualenv
@functools.lru_cache(maxsize=None)
@functools.cache
def get_macos_version() -> tuple[int, int]:
"""
Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0)
@@ -74,10 +60,10 @@ def get_macos_version() -> tuple[int, int]:
capture_stdout=True,
)
version = tuple(map(int, version_str.split(".")[:2]))
return typing.cast(Tuple[int, int], version)
return typing.cast(tuple[int, int], version)
@functools.lru_cache(maxsize=None)
@functools.cache
def get_test_macosx_deployment_target() -> str:
version = get_macos_version()
if version >= (11, 0):
@@ -90,19 +76,22 @@ def get_macos_sdks() -> list[str]:
return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
@dataclass(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class PythonConfiguration:
version: str
identifier: str
url: str
def all_python_configurations() -> list[PythonConfiguration]:
config_dicts = resources.read_python_configs("macos")
return [PythonConfiguration(**item) for item in config_dicts]
def get_python_configurations(
build_selector: BuildSelector, architectures: Set[Architecture]
) -> list[PythonConfiguration]:
full_python_configs = read_python_configs("macos")
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
python_configurations = all_python_configurations()
# filter out configs that don't match any of the selected architectures
python_configurations = [
@@ -114,7 +103,7 @@ def get_python_configurations(
# skip builds as required by BUILD/SKIP
python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
# filter-out some cross-compilation configs with PyPy:
# filter-out some cross-compilation configs with PyPy and GraalPy:
# can't build arm64 on x86_64
# rosetta allows to build x86_64 on arm64
if platform.machine() == "x86_64":
@@ -122,7 +111,7 @@ def get_python_configurations(
python_configurations = [
c
for c in python_configurations
if not (c.identifier.startswith("pp") and c.identifier.endswith("arm64"))
if not (c.identifier.startswith(("pp", "gp")) and c.identifier.endswith("arm64"))
]
removed_elements = python_configurations_before - set(python_configurations)
if removed_elements:
@@ -130,7 +119,7 @@ def get_python_configurations(
log.quiet(
unwrap(
f"""
Note: {ids} {'was' if len(removed_elements) == 1 else 'were'}
Note: {ids} {"was" if len(removed_elements) == 1 else "were"}
selected, but can't be built on x86_64 so will be skipped automatically.
"""
)
@@ -139,7 +128,7 @@ def get_python_configurations(
return python_configurations
def install_cpython(tmp: Path, version: str, url: str, free_threading: bool) -> Path:
def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) -> Path:
ft = "T" if free_threading else ""
installation_path = Path(f"/Library/Frameworks/Python{ft}.framework/Versions/{version}")
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
@@ -155,20 +144,22 @@ def install_cpython(tmp: Path, version: str, url: str, free_threading: bool) ->
Error: CPython {version} is not installed.
cibuildwheel will not perform system-wide installs when running outside of CI.
To build locally, install CPython {version} on this machine, or, disable this
version of Python using CIBW_SKIP=cp{version.replace('.', '')}-macosx_*
version of Python using CIBW_SKIP=cp{version.replace(".", "")}-macosx_*
For portable builds, cibuildwheel needs the official builds from python.org.
Download link: {url}
"""
)
raise errors.FatalError(msg)
pkg_path = tmp / "Python.pkg"
# download the pkg
download(url, pkg_path)
# install
python_filename = url.split("/")[-1]
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename
if not pkg_path.exists():
download(url, pkg_path)
args = []
if version.startswith("3.13"):
# Python 3.13 is the first version to have a free-threading option
args += ["-applyChoiceChangesXML", str(free_thread_enable_313.resolve())]
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_313.resolve())]
elif version.startswith("3.14"):
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_314.resolve())]
call("sudo", "installer", "-pkg", pkg_path, *args, "-target", "/")
pkg_path.unlink()
env = os.environ.copy()
@@ -176,9 +167,13 @@ def install_cpython(tmp: Path, version: str, url: str, free_threading: bool) ->
if free_threading:
call(installation_path / f"bin/python{version}t", "-m", "ensurepip", env=env)
call(installation_path / f"bin/python{version}t", install_certifi_script, env=env)
call(
installation_path / f"bin/python{version}t",
resources.INSTALL_CERTIFI_SCRIPT,
env=env,
)
else:
call(installation_path / "bin/python3", install_certifi_script, env=env)
call(installation_path / "bin/python3", resources.INSTALL_CERTIFI_SCRIPT, env=env)
return installation_path / "bin" / (f"python{version}t" if free_threading else "python3")
@@ -198,21 +193,29 @@ def install_pypy(tmp: Path, url: str) -> Path:
return installation_path / "bin" / "pypy3"
def can_use_uv(python_configuration: PythonConfiguration) -> bool:
conditions = (Version(python_configuration.version) >= Version("3.8"),)
return all(conditions)
def install_graalpy(tmp: Path, url: str) -> Path:
graalpy_archive = url.rsplit("/", 1)[-1]
extension = ".tar.gz"
assert graalpy_archive.endswith(extension)
installation_path = CIBW_CACHE_PATH / graalpy_archive[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
downloaded_archive = tmp / graalpy_archive
download(url, downloaded_archive)
installation_path.mkdir(parents=True)
# GraalPy top-folder name is inconsistent with archive name
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
downloaded_archive.unlink()
return installation_path / "bin" / "graalpy"
def setup_python(
tmp: Path,
python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr],
dependency_constraint: Path | None,
environment: ParsedEnvironment,
build_frontend: BuildFrontendName,
) -> tuple[Path, dict[str, str]]:
if build_frontend == "build[uv]" and not can_use_uv(python_configuration):
build_frontend = "build"
uv_path = find_uv()
use_uv = build_frontend == "build[uv]"
@@ -227,12 +230,14 @@ def setup_python(
elif implementation_id.startswith("pp"):
base_python = install_pypy(tmp, python_configuration.url)
elif implementation_id.startswith("gp"):
base_python = install_graalpy(tmp, python_configuration.url)
else:
msg = "Unknown Python implementation"
raise ValueError(msg)
assert (
base_python.exists()
), f"{base_python.name} not found, has {list(base_python.parent.iterdir())}"
assert base_python.exists(), (
f"{base_python.name} not found, has {list(base_python.parent.iterdir())}"
)
log.step("Setting up build environment...")
venv_path = tmp / "venv"
@@ -240,7 +245,7 @@ def setup_python(
python_configuration.version,
base_python,
venv_path,
dependency_constraint_flags,
dependency_constraint,
use_uv=use_uv,
)
venv_bin_path = venv_path / "bin"
@@ -256,25 +261,6 @@ def setup_python(
# we version pip ourselves, so we don't care about pip version checking
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
# upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
if build_frontend == "build[uv]":
assert uv_path is not None
pip = [str(uv_path), "pip"]
else:
pip = ["python", "-m", "pip"]
if not use_uv:
call(
*pip,
"install",
"--upgrade",
"pip",
*dependency_constraint_flags,
env=env,
cwd=venv_path,
)
# Apply our environment after pip is ready
env = environment.as_dictionary(prev_environment=env)
@@ -282,7 +268,7 @@ def setup_python(
which_python = call("which", "python", env=env, capture_stdout=True).strip()
print(which_python)
if which_python != str(venv_bin_path / "python"):
msg = "cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it."
msg = "python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it."
raise errors.FatalError(msg)
call("python", "--version", env=env)
@@ -292,7 +278,7 @@ def setup_python(
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
print(which_pip)
if which_pip != str(venv_bin_path / "pip"):
msg = "cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
msg = "pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it."
raise errors.FatalError(msg)
call("pip", "--version", env=env)
@@ -322,20 +308,19 @@ def setup_python(
)
env["MACOSX_DEPLOYMENT_TARGET"] = default_target
if python_configuration.version not in {"3.6", "3.7"}:
if config_is_arm64:
# macOS 11 is the first OS with arm64 support, so the wheels
# have that as a minimum.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64")
env.setdefault("ARCHFLAGS", "-arch arm64")
elif config_is_universal2:
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2")
env.setdefault("ARCHFLAGS", "-arch arm64 -arch x86_64")
elif python_configuration.identifier.endswith("x86_64"):
# even on the macos11.0 Python installer, on the x86_64 side it's
# compatible back to 10.9.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-x86_64")
env.setdefault("ARCHFLAGS", "-arch x86_64")
if config_is_arm64:
# macOS 11 is the first OS with arm64 support, so the wheels
# have that as a minimum.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-11.0-arm64")
env.setdefault("ARCHFLAGS", "-arch arm64")
elif config_is_universal2:
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-universal2")
env.setdefault("ARCHFLAGS", "-arch arm64 -arch x86_64")
elif python_configuration.identifier.endswith("x86_64"):
# even on the macos11.0 Python installer, on the x86_64 side it's
# compatible back to 10.9.
env.setdefault("_PYTHON_HOST_PLATFORM", "macosx-10.9-x86_64")
env.setdefault("ARCHFLAGS", "-arch x86_64")
building_arm64 = config_is_arm64 or config_is_universal2
if building_arm64 and get_macos_version() < (10, 16) and "SDKROOT" not in env:
@@ -360,39 +345,40 @@ def setup_python(
env.setdefault("SDKROOT", arm64_compatible_sdks[0])
log.step("Installing build tools...")
if build_frontend == "pip":
call(
"pip",
"install",
"--upgrade",
"delocate",
*dependency_constraint_flags,
env=env,
)
elif build_frontend == "build":
call(
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv]",
*dependency_constraint_flags,
env=env,
)
elif build_frontend == "build[uv]":
assert uv_path is not None
call(
uv_path,
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv, uv]",
*dependency_constraint_flags,
env=env,
)
else:
assert_never(build_frontend)
match build_frontend:
case "pip":
call(
"pip",
"install",
"--upgrade",
"delocate",
*constraint_flags(dependency_constraint),
env=env,
)
case "build":
call(
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv]",
*constraint_flags(dependency_constraint),
env=env,
)
case "build[uv]":
assert uv_path is not None
call(
uv_path,
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv, uv]",
*constraint_flags(dependency_constraint),
env=env,
)
case _:
assert_never(build_frontend)
return base_python, env
@@ -422,8 +408,8 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
use_uv = build_frontend.name == "build[uv]" and can_use_uv(config)
build_frontend = build_options.build_frontend
use_uv = build_frontend.name == "build[uv]"
uv_path = find_uv()
if use_uv and uv_path is None:
msg = "uv not found"
@@ -439,22 +425,18 @@ def build(options: Options, tmp_path: Path) -> None:
config_is_arm64 = config.identifier.endswith("arm64")
config_is_universal2 = config.identifier.endswith("universal2")
dependency_constraint_flags: Sequence[PathOrStr] = []
if build_options.dependency_constraints:
dependency_constraint_flags = [
"-c",
build_options.dependency_constraints.get_for_python_version(config.version),
]
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, tmp_dir=identifier_tmp_dir
)
base_python, env = setup_python(
identifier_tmp_dir / "build",
config,
dependency_constraint_flags,
constraints_path,
build_options.environment,
build_frontend.name,
)
if not use_uv:
pip_version = get_pip_version(env)
pip_version = None if use_uv else get_pip_version(env)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
@@ -474,55 +456,52 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Building wheel...")
built_wheel_dir.mkdir()
extra_flags = split_config_settings(
build_options.config_settings, build_frontend.name
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
)
extra_flags += build_frontend.args
build_env = env.copy()
if not use_uv:
if pip_version is not None:
build_env["VIRTUALENV_PIP"] = pip_version
if build_options.dependency_constraints:
constraint_path = build_options.dependency_constraints.get_for_python_version(
config.version
)
if constraints_path:
combine_constraints(
build_env, constraint_path, identifier_tmp_dir if use_uv else None
build_env, constraints_path, identifier_tmp_dir if use_uv else None
)
if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
build_options.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
env=build_env,
)
elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg)
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
extra_flags.append("--installer=uv")
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
else:
assert_never(build_frontend)
match build_frontend.name:
case "pip":
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
build_options.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
env=build_env,
)
case "build" | "build[uv]":
if (
use_uv
and "--no-isolation" not in extra_flags
and "-n" not in extra_flags
):
extra_flags.append("--installer=uv")
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
case _:
assert_never(build_frontend)
built_wheel = next(built_wheel_dir.glob("*.whl"))
@@ -641,13 +620,6 @@ def build(options: Options, tmp_path: Path) -> None:
else f"Testing wheel on {testing_arch}..."
)
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
if not use_uv:
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}"
arch_prefix = []
uv_arch_args = []
if testing_arch != machine_arch:
@@ -663,27 +635,28 @@ def build(options: Options, tmp_path: Path) -> None:
call_with_arch = functools.partial(call, *arch_prefix)
shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c")
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}"
virtualenv_env = virtualenv(
config.version,
base_python,
venv_dir,
None,
use_uv=use_uv,
env=env,
pip_version=pip_version,
)
if use_uv:
pip_install = functools.partial(call, *pip, "install", *uv_arch_args)
call("uv", "venv", venv_dir, f"--python={base_python}", env=env)
else:
pip_install = functools.partial(call_with_arch, *pip, "install")
# Use pip version from the initial env to ensure determinism
venv_args = ["--no-periodic-update", f"--pip={pip_version}"]
# In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones
if Version(config.version) < Version("3.12"):
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
call_with_arch("python", "-m", "virtualenv", *venv_args, venv_dir, env=env)
virtualenv_env = env.copy()
virtualenv_env["MACOSX_DEPLOYMENT_TARGET"] = get_test_macosx_deployment_target()
virtualenv_env["PATH"] = os.pathsep.join(
[
str(venv_dir / "bin"),
virtualenv_env["PATH"],
]
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
)
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
# check that we are using the Python from the virtual environment
call_with_arch("which", "python", env=virtualenv_env)
@@ -731,31 +704,48 @@ def build(options: Options, tmp_path: Path) -> None:
# and not the repo code)
test_command_prepared = prepare_command(
build_options.test_command,
project=Path(".").resolve(),
project=Path.cwd(),
package=build_options.package_dir.resolve(),
wheel=repaired_wheel,
)
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir(exist_ok=True)
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
if build_options.test_sources:
# only create test_cwd if it doesn't already exist - it
# may have been created during a previous `testing_arch`
if not test_cwd.exists():
test_cwd.mkdir()
copy_test_sources(
build_options.test_sources,
Path.cwd(),
test_cwd,
)
else:
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
test_cwd.mkdir(exist_ok=True)
(test_cwd / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text()
)
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (overwrite existing)
output_wheel = None
if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
moved_wheel = move_file(repaired_wheel, output_wheel)
if moved_wheel != output_wheel.resolve():
log.warning(
"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
)
built_wheels.append(output_wheel)
# clean up
shutil.rmtree(identifier_tmp_dir)
log.build_end()
log.build_end(output_wheel)
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
@@ -1,54 +1,100 @@
from __future__ import annotations
import dataclasses
import functools
import json
import os
import shutil
import subprocess
import sys
from collections.abc import Sequence, Set
from dataclasses import dataclass
import tomllib
import typing
from collections.abc import Set
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Final, TypedDict
from filelock import FileLock
from . import errors
from .architecture import Architecture
from .environment import ParsedEnvironment
from .logger import log
from .options import Options
from .typing import PathOrStr
from .util import (
from .. import errors
from ..architecture import Architecture
from ..environment import ParsedEnvironment
from ..frontend import get_build_frontend_extra_flags
from ..logger import log
from ..options import Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.file import (
CIBW_CACHE_PATH,
BuildFrontendConfig,
BuildSelector,
call,
combine_constraints,
copy_test_sources,
download,
ensure_node,
extract_tar,
extract_zip,
find_compatible_wheel,
get_pip_version,
move_file,
prepare_command,
read_python_configs,
shell,
split_config_settings,
test_fail_cwd_file,
virtualenv,
)
from ..util.helpers import prepare_command, unwrap, unwrap_preserving_paragraphs
from ..util.packaging import combine_constraints, find_compatible_wheel, get_pip_version
from ..util.python_build_standalone import (
PythonBuildStandaloneError,
create_python_build_standalone_environment,
)
from ..venv import constraint_flags, virtualenv
IS_WIN: Final[bool] = sys.platform.startswith("win")
@dataclass(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class PythonConfiguration:
version: str
identifier: str
pyodide_version: str
pyodide_build_version: str
emscripten_version: str
default_pyodide_version: str
node_version: str
class PyodideXBuildEnvInfoVersionRange(TypedDict):
min: str | None
max: str | None
class PyodideXBuildEnvInfo(TypedDict):
version: str
python: str
emscripten: str
pyodide_build: PyodideXBuildEnvInfoVersionRange
compatible: bool
@functools.cache
def ensure_node(major_version: str) -> Path:
with resources.NODEJS.open("rb") as f:
loaded_file = tomllib.load(f)
version = str(loaded_file[major_version])
base_url = str(loaded_file["url"])
ext = "zip" if IS_WIN else "tar.xz"
platform = "win" if IS_WIN else ("darwin" if sys.platform.startswith("darwin") else "linux")
linux_arch = Architecture.native_arch("linux")
assert linux_arch is not None
arch = {"x86_64": "x64", "i686": "x86", "aarch64": "arm64"}.get(
linux_arch.value, linux_arch.value
)
name = f"node-{version}-{platform}-{arch}"
path = CIBW_CACHE_PATH / name
with FileLock(str(path) + ".lock"):
if not path.exists():
url = f"{base_url}{version}/{name}.{ext}"
with TemporaryDirectory() as tmp_path:
archive = Path(tmp_path) / f"{name}.{ext}"
download(url, archive)
if ext == "zip":
extract_zip(archive, path.parent)
else:
extract_tar(archive, path.parent)
assert path.exists()
if not IS_WIN:
return path / "bin"
return path
def install_emscripten(tmp: Path, version: str) -> Path:
# We don't need to match the emsdk version to the version we install, but
# we do for stability
url = f"https://github.com/emscripten-core/emsdk/archive/refs/tags/{version}.zip"
installation_path = CIBW_CACHE_PATH / f"emsdk-{version}"
emsdk_path = installation_path / f"emsdk-{version}/emsdk"
@@ -66,16 +112,82 @@ def install_emscripten(tmp: Path, version: str) -> Path:
return emcc_path
def get_all_xbuildenv_version_info(env: dict[str, str]) -> list[PyodideXBuildEnvInfo]:
xbuildenvs_info_str = call(
"pyodide",
"xbuildenv",
"search",
"--json",
"--all",
env=env,
cwd=CIBW_CACHE_PATH,
capture_stdout=True,
).strip()
xbuildenvs_info = json.loads(xbuildenvs_info_str)
if "environments" not in xbuildenvs_info:
msg = f"Invalid xbuildenvs info, got {xbuildenvs_info}"
raise ValueError(msg)
return typing.cast(list[PyodideXBuildEnvInfo], xbuildenvs_info["environments"])
def get_xbuildenv_version_info(
env: dict[str, str], version: str, pyodide_build_version: str
) -> PyodideXBuildEnvInfo:
xbuildenvs_info = get_all_xbuildenv_version_info(env)
for xbuildenv_info in xbuildenvs_info:
if xbuildenv_info["version"] == version:
return xbuildenv_info
msg = unwrap(f"""
Could not find Pyodide cross-build environment version {version} in the available
versions as reported by pyodide-build v{pyodide_build_version}.
Available pyodide xbuildenv versions are:
{", ".join(e["version"] for e in xbuildenvs_info if e["compatible"])}
""")
raise errors.FatalError(msg)
# The default pyodide xbuildenv version that's specified in
# build-platforms.toml is compatible with the pyodide-build version that's
# pinned in the bundled constraints file. But if the user changes
# pyodide-version and/or dependency-constraints in the cibuildwheel config, we
# need to check if the xbuildenv version is compatible with the pyodide-build
# version.
def validate_pyodide_build_version(
xbuildenv_info: PyodideXBuildEnvInfo, pyodide_build_version: str
) -> None:
"""
Validate the Pyodide version is compatible with the installed
pyodide-build version.
"""
pyodide_version = xbuildenv_info["version"]
if not xbuildenv_info["compatible"]:
msg = unwrap_preserving_paragraphs(f"""
The Pyodide xbuildenv version {pyodide_version} is not compatible
with the pyodide-build version {pyodide_build_version}. Please use
the 'pyodide xbuildenv search --all' command to find a compatible
version.
Set the pyodide-build version using the `dependency-constraints`
option, or set the Pyodide xbuildenv version using the
`pyodide-version` option.
""")
raise errors.FatalError(msg)
def install_xbuildenv(env: dict[str, str], pyodide_build_version: str, pyodide_version: str) -> str:
"""Install a particular Pyodide xbuildenv version and set a path to the Pyodide root."""
# Since pyodide-build was unvendored from Pyodide v0.27.0, the versions of pyodide-build are
# not guaranteed to match the versions of Pyodide or be in sync with them. Hence, we shall
# specify the pyodide-build version in the root path, which will set up the xbuildenv for
# the requested Pyodide version.
pyodide_root = (
CIBW_CACHE_PATH
/ f".pyodide-xbuildenv-{pyodide_build_version}/{pyodide_version}/xbuildenv/pyodide-root"
)
# Since pyodide-build was unvendored from Pyodide v0.27.0, the versions of
# pyodide-build are uncoupled from the versions of Pyodide. So, we specify
# both the pyodide-build version and the Pyodide version in the temp path.
xbuildenv_cache_path = CIBW_CACHE_PATH / f"pyodide-build-{pyodide_build_version}"
pyodide_root = xbuildenv_cache_path / pyodide_version / "xbuildenv" / "pyodide-root"
with FileLock(CIBW_CACHE_PATH / "xbuildenv.lock"):
if pyodide_root.exists():
return str(pyodide_root)
@@ -84,44 +196,52 @@ def install_xbuildenv(env: dict[str, str], pyodide_build_version: str, pyodide_v
# PYODIDE_ROOT so copy it first.
env = dict(env)
env.pop("PYODIDE_ROOT", None)
# Install the xbuildenv
call(
"pyodide",
"xbuildenv",
"install",
"--path",
str(xbuildenv_cache_path),
pyodide_version,
env=env,
cwd=CIBW_CACHE_PATH,
)
assert pyodide_root.exists()
return str(pyodide_root)
def get_base_python(identifier: str) -> Path:
implementation_id = identifier.split("-")[0]
majorminor = implementation_id[len("cp") :]
version_info = (int(majorminor[0]), int(majorminor[1:]))
if version_info == sys.version_info[:2]:
return Path(sys.executable)
major_minor = ".".join(str(v) for v in version_info)
python_name = f"python{major_minor}"
which_python = shutil.which(python_name)
if which_python is None:
msg = f"CPython {major_minor} is not installed."
raise errors.FatalError(msg)
return Path(which_python)
def get_base_python(tmp: Path, python_configuration: PythonConfiguration) -> Path:
try:
return create_python_build_standalone_environment(
python_version=python_configuration.version,
temp_dir=tmp,
cache_dir=CIBW_CACHE_PATH,
)
except PythonBuildStandaloneError as e:
msg = unwrap(f"""
Failed to create a Python build environment:
{e}
""")
raise errors.FatalError(msg) from e
def setup_python(
tmp: Path,
python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr],
constraints_path: Path | None,
environment: ParsedEnvironment,
user_pyodide_version: str | None,
) -> dict[str, str]:
base_python = get_base_python(python_configuration.identifier)
log.step("Installing a base python environment...")
base_python = get_base_python(tmp / "base", python_configuration)
log.step("Setting up build environment...")
pyodide_version = user_pyodide_version or python_configuration.default_pyodide_version
venv_path = tmp / "venv"
env = virtualenv(python_configuration.version, base_python, venv_path, [], use_uv=False)
env = virtualenv(python_configuration.version, base_python, venv_path, None, use_uv=False)
venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
@@ -135,7 +255,7 @@ def setup_python(
"install",
"--upgrade",
"pip",
*dependency_constraint_flags,
*constraint_flags(constraints_path),
env=env,
cwd=venv_path,
)
@@ -165,33 +285,48 @@ def setup_python(
"install",
"--upgrade",
"auditwheel-emscripten",
"build[virtualenv]",
"pyodide-build",
*dependency_constraint_flags,
*constraint_flags(constraints_path),
env=env,
)
log.step(f"Installing Emscripten version: {python_configuration.emscripten_version} ...")
emcc_path = install_emscripten(tmp, python_configuration.emscripten_version)
pyodide_build_version = call(
"python",
"-c",
"from importlib.metadata import version; print(version('pyodide-build'))",
env=env,
capture_stdout=True,
).strip()
xbuildenv_info = get_xbuildenv_version_info(env, pyodide_version, pyodide_build_version)
validate_pyodide_build_version(
xbuildenv_info=xbuildenv_info,
pyodide_build_version=pyodide_build_version,
)
emscripten_version = xbuildenv_info["emscripten"]
log.step(f"Installing Emscripten version: {emscripten_version} ...")
emcc_path = install_emscripten(tmp, emscripten_version)
env["PATH"] = os.pathsep.join([str(emcc_path.parent), env["PATH"]])
log.step(f"Installing Pyodide xbuildenv version: {python_configuration.pyodide_version} ...")
env["PYODIDE_ROOT"] = install_xbuildenv(
env, python_configuration.pyodide_build_version, python_configuration.pyodide_version
)
log.step(f"Installing Pyodide xbuildenv version: {pyodide_version} ...")
env["PYODIDE_ROOT"] = install_xbuildenv(env, pyodide_build_version, pyodide_version)
return env
def all_python_configurations() -> list[PythonConfiguration]:
full_python_configs = resources.read_python_configs("pyodide")
return [PythonConfiguration(**item) for item in full_python_configs]
def get_python_configurations(
build_selector: BuildSelector,
architectures: Set[Architecture], # noqa: ARG001
) -> list[PythonConfiguration]:
full_python_configs = read_python_configs("pyodide")
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
return python_configurations
return [c for c in all_python_configurations() if build_selector(c.identifier)]
def build(options: Options, tmp_path: Path) -> None:
@@ -218,7 +353,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("build")
build_frontend = build_options.build_frontend
if build_frontend.name == "pip":
msg = "The pyodide platform doesn't support pip frontend"
@@ -227,24 +362,23 @@ def build(options: Options, tmp_path: Path) -> None:
log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier
built_wheel_dir = identifier_tmp_dir / "built_wheel"
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
identifier_tmp_dir.mkdir()
built_wheel_dir.mkdir()
repaired_wheel_dir.mkdir()
dependency_constraint_flags: Sequence[PathOrStr] = []
if build_options.dependency_constraints:
constraints_path = build_options.dependency_constraints.get_for_python_version(
config.version, variant="pyodide"
)
dependency_constraint_flags = ["-c", constraints_path]
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, variant="pyodide", tmp_dir=identifier_tmp_dir
)
env = setup_python(
identifier_tmp_dir / "build",
config,
dependency_constraint_flags,
build_options.environment,
tmp=identifier_tmp_dir / "build",
python_configuration=config,
constraints_path=constraints_path,
environment=build_options.environment,
user_pyodide_version=build_options.pyodide_version,
)
pip_version = get_pip_version(env)
# The Pyodide command line runner mounts all directories in the host
@@ -258,8 +392,8 @@ def build(options: Options, tmp_path: Path) -> None:
# directory.
oldmounts = ""
extra_mounts = [str(identifier_tmp_dir)]
if str(Path(".").resolve()).startswith("/tmp"):
extra_mounts.append(str(Path(".").resolve()))
if Path.cwd().is_relative_to("/tmp"):
extra_mounts.append(str(Path.cwd()))
if "_PYODIDE_EXTRA_MOUNTS" in env:
oldmounts = env["_PYODIDE_EXTRA_MOUNTS"] + ":"
@@ -282,15 +416,12 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Building wheel...")
extra_flags = split_config_settings(build_options.config_settings, "build")
extra_flags += build_frontend.args
if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg)
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
)
build_env = env.copy()
if build_options.dependency_constraints:
if constraints_path:
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
build_env["VIRTUALENV_PIP"] = pip_version
call(
@@ -315,6 +446,7 @@ def build(options: Options, tmp_path: Path) -> None:
dest_dir=repaired_wheel_dir,
)
shell(repair_command_prepared, env=env)
log.step_end()
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
@@ -354,6 +486,10 @@ def build(options: Options, tmp_path: Path) -> None:
)
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
)
# check that we are using the Python from the virtual environment
call("which", "python", env=virtualenv_env)
@@ -362,6 +498,7 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.before_test,
project=".",
package=build_options.package_dir,
wheel=repaired_wheel,
)
shell(before_test_prepared, env=virtualenv_env)
@@ -382,25 +519,38 @@ def build(options: Options, tmp_path: Path) -> None:
# and not the repo code)
test_command_prepared = prepare_command(
build_options.test_command,
project=Path(".").resolve(),
project=Path.cwd(),
package=build_options.package_dir.resolve(),
)
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir(exist_ok=True)
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
Path.cwd(),
test_cwd,
)
else:
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
(test_cwd / "test_fail.py").write_text(resources.TEST_FAIL_CWD_FILE.read_text())
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (overwrite existing)
output_wheel: Path | None = None
if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
moved_wheel = move_file(repaired_wheel, output_wheel)
if moved_wheel != output_wheel.resolve():
log.warning(
"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
)
built_wheels.append(output_wheel)
log.build_end(output_wheel)
finally:
pass
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
@@ -1,47 +1,30 @@
from __future__ import annotations
import dataclasses
import json
import os
import platform as platform_module
import shutil
import subprocess
import textwrap
from collections.abc import MutableMapping, Sequence, Set
from dataclasses import dataclass
from functools import lru_cache
from collections.abc import MutableMapping, Set
from functools import cache
from pathlib import Path
from typing import assert_never
from filelock import FileLock
from packaging.version import Version
from . import errors
from ._compat.typing import assert_never
from .architecture import Architecture
from .environment import ParsedEnvironment
from .logger import log
from .options import Options
from .typing import PathOrStr
from .util import (
CIBW_CACHE_PATH,
BuildFrontendConfig,
BuildFrontendName,
BuildSelector,
call,
combine_constraints,
download,
extract_zip,
find_compatible_wheel,
find_uv,
get_build_verbosity_extra_flags,
get_pip_version,
move_file,
prepare_command,
read_python_configs,
shell,
split_config_settings,
test_fail_cwd_file,
unwrap,
virtualenv,
)
from .. import errors
from ..architecture import Architecture
from ..environment import ParsedEnvironment
from ..frontend import BuildFrontendName, get_build_frontend_extra_flags
from ..logger import log
from ..options import Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, extract_zip, move_file
from ..util.helpers import prepare_command, unwrap
from ..util.packaging import combine_constraints, find_compatible_wheel, get_pip_version
from ..venv import constraint_flags, find_uv, virtualenv
def get_nuget_args(
@@ -68,7 +51,7 @@ def get_nuget_args(
]
@dataclass(frozen=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class PythonConfiguration:
version: str
arch: str
@@ -76,13 +59,16 @@ class PythonConfiguration:
url: str | None = None
def all_python_configurations() -> list[PythonConfiguration]:
config_dicts = resources.read_python_configs("windows")
return [PythonConfiguration(**item) for item in config_dicts]
def get_python_configurations(
build_selector: BuildSelector,
architectures: Set[Architecture],
) -> list[PythonConfiguration]:
full_python_configs = read_python_configs("windows")
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
python_configurations = all_python_configurations()
map_arch = {"32": Architecture.x86, "64": Architecture.AMD64, "ARM64": Architecture.ARM64}
@@ -96,7 +82,7 @@ def get_python_configurations(
return python_configurations
@lru_cache(maxsize=None)
@cache
def _ensure_nuget() -> Path:
nuget = CIBW_CACHE_PATH / "nuget.exe"
with FileLock(str(nuget) + ".lock"):
@@ -138,6 +124,20 @@ def install_pypy(tmp: Path, arch: str, url: str) -> Path:
return installation_path / "python.exe"
def install_graalpy(tmp: Path, url: str) -> Path:
zip_filename = url.rsplit("/", 1)[-1]
extension = ".zip"
assert zip_filename.endswith(extension)
installation_path = CIBW_CACHE_PATH / zip_filename[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
graalpy_zip = tmp / zip_filename
download(url, graalpy_zip)
# Extract to the parent directory because the zip file still contains a directory
extract_zip(graalpy_zip, installation_path.parent)
return installation_path / "bin" / "graalpy.exe"
def setup_setuptools_cross_compile(
tmp: Path,
python_configuration: PythonConfiguration,
@@ -226,17 +226,14 @@ def setup_rust_cross_compile(
def can_use_uv(python_configuration: PythonConfiguration) -> bool:
conditions = (
Version(python_configuration.version) >= Version("3.8"),
not python_configuration.identifier.startswith("pp38-"),
)
conditions = (not python_configuration.identifier.startswith("pp38-"),)
return all(conditions)
def setup_python(
tmp: Path,
python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr],
dependency_constraint: Path | None,
environment: ParsedEnvironment,
build_frontend: BuildFrontendName,
) -> tuple[Path, dict[str, str]]:
@@ -257,6 +254,8 @@ def setup_python(
elif implementation_id.startswith("pp"):
assert python_configuration.url is not None
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
elif implementation_id.startswith("gp"):
base_python = install_graalpy(tmp, python_configuration.url or "")
else:
msg = "Unknown Python implementation"
raise ValueError(msg)
@@ -274,7 +273,7 @@ def setup_python(
python_configuration.version,
base_python,
venv_path,
dependency_constraint_flags,
dependency_constraint,
use_uv=use_uv,
)
@@ -283,21 +282,6 @@ def setup_python(
env["PYTHON_ARCH"] = python_configuration.arch
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
# upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip.exe'
if not use_uv:
call(
"python",
"-m",
"pip",
"install",
"--upgrade",
"pip",
*dependency_constraint_flags,
env=env,
cwd=venv_path,
)
# update env with results from CIBW_ENVIRONMENT
env = environment.as_dictionary(prev_environment=env)
@@ -321,32 +305,69 @@ def setup_python(
call("pip", "--version", env=env)
log.step("Installing build tools...")
if build_frontend == "build":
call(
"pip",
"install",
"--upgrade",
"build[virtualenv]",
*dependency_constraint_flags,
env=env,
)
elif build_frontend == "build[uv]":
assert uv_path is not None
call(
uv_path,
"pip",
"install",
"--upgrade",
"build[virtualenv]",
*dependency_constraint_flags,
env=env,
)
match build_frontend:
case "build":
call(
"pip",
"install",
"--upgrade",
"build[virtualenv]",
*constraint_flags(dependency_constraint),
env=env,
)
case "build[uv]":
assert uv_path is not None
call(
uv_path,
"pip",
"install",
"--upgrade",
"build[virtualenv]",
*constraint_flags(dependency_constraint),
env=env,
)
if python_libs_base:
# Set up the environment for various backends to enable cross-compilation
setup_setuptools_cross_compile(tmp, python_configuration, python_libs_base, env)
setup_rust_cross_compile(tmp, python_configuration, python_libs_base, env)
if implementation_id.startswith("gp"):
# GraalPy fails to discover compilers, setup the relevant environment
# variables. Adapted from
# https://github.com/microsoft/vswhere/wiki/Start-Developer-Command-Prompt
# Remove when https://github.com/oracle/graalpython/issues/492 is fixed.
vcpath = call(
Path(os.environ["PROGRAMFILES(X86)"])
/ "Microsoft Visual Studio"
/ "Installer"
/ "vswhere.exe",
"-products",
"*",
"-latest",
"-property",
"installationPath",
capture_stdout=True,
).strip()
log.notice(f"Discovering Visual Studio for GraalPy at {vcpath}")
vcvars_file = tmp / "vcvars.json"
call(
f"{vcpath}\\Common7\\Tools\\vsdevcmd.bat",
"-no_logo",
"-arch=amd64",
"-host_arch=amd64",
"&&",
"python",
"-c",
# this command needs to be one line for Windows reasons
"import sys, json, pathlib, os; pathlib.Path(sys.argv[1]).write_text(json.dumps(dict(os.environ)))",
vcvars_file,
env=env,
)
with open(vcvars_file, encoding="utf-8") as f:
vcvars = json.load(f)
env.update(vcvars)
return base_python, env
@@ -374,7 +395,8 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
build_frontend = build_options.build_frontend
use_uv = build_frontend.name == "build[uv]" and can_use_uv(config)
log.build_start(config.identifier)
@@ -383,23 +405,20 @@ def build(options: Options, tmp_path: Path) -> None:
built_wheel_dir = identifier_tmp_dir / "built_wheel"
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
dependency_constraint_flags: Sequence[PathOrStr] = []
if build_options.dependency_constraints:
dependency_constraint_flags = [
"-c",
build_options.dependency_constraints.get_for_python_version(config.version),
]
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version,
tmp_dir=identifier_tmp_dir,
)
# install Python
base_python, env = setup_python(
identifier_tmp_dir / "build",
config,
dependency_constraint_flags,
constraints_path,
build_options.environment,
build_frontend.name,
)
if not use_uv:
pip_version = get_pip_version(env)
pip_version = None if use_uv else get_pip_version(env)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel:
@@ -422,54 +441,68 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Building wheel...")
built_wheel_dir.mkdir()
extra_flags = split_config_settings(
build_options.config_settings, build_frontend.name
extra_flags = get_build_frontend_extra_flags(
build_frontend, build_options.build_verbosity, build_options.config_settings
)
extra_flags += build_frontend.args
if (
config.identifier.startswith("gp")
and build_frontend.name == "build"
and "--no-isolation" not in extra_flags
and "-n" not in extra_flags
):
# GraalPy fails to discover its standard library when a venv is created
# from a virtualenv seeded executable. See
# https://github.com/oracle/graalpython/issues/491 and remove this once
# fixed upstream.
log.notice(
"Disabling build isolation to workaround GraalPy bug. If the build fails, consider using pip or build[uv] as build frontend."
)
shell("graalpy -m pip install setuptools wheel", env=env)
extra_flags = [*extra_flags, "-n"]
build_env = env.copy()
if not use_uv:
if pip_version is not None:
build_env["VIRTUALENV_PIP"] = pip_version
if build_options.dependency_constraints:
constraints_path = build_options.dependency_constraints.get_for_python_version(
config.version
)
if constraints_path:
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
options.globals.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
env=build_env,
)
elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg)
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
extra_flags.append("--installer=uv")
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
else:
assert_never(build_frontend)
match build_frontend.name:
case "pip":
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
options.globals.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*extra_flags,
env=build_env,
)
case "build" | "build[uv]":
if (
use_uv
and "--no-isolation" not in extra_flags
and "-n" not in extra_flags
):
extra_flags.append("--installer=uv")
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
*extra_flags,
env=build_env,
)
case _:
assert_never(build_frontend)
built_wheel = next(built_wheel_dir.glob("*.whl"))
@@ -514,29 +547,20 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Testing wheel...")
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
if not use_uv:
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
venv_dir = identifier_tmp_dir / "venv-test"
virtualenv_env = virtualenv(
config.version,
base_python,
venv_dir,
None,
use_uv=use_uv,
env=env,
pip_version=pip_version,
)
if use_uv:
call("uv", "venv", venv_dir, f"--python={base_python}", env=env)
else:
# Use pip version from the initial env to ensure determinism
venv_args = ["--no-periodic-update", f"--pip={pip_version}"]
# In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones
if Version(config.version) < Version("3.12"):
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
call("python", "-m", "virtualenv", *venv_args, venv_dir, env=env)
virtualenv_env = env.copy()
virtualenv_env["PATH"] = os.pathsep.join(
[
str(venv_dir / "Scripts"),
virtualenv_env["PATH"],
]
virtualenv_env = build_options.test_environment.as_dictionary(
prev_environment=virtualenv_env
)
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
# check that we are using the Python from the virtual environment
call("where", "python", env=virtualenv_env)
@@ -566,25 +590,36 @@ def build(options: Options, tmp_path: Path) -> None:
# run the tests from a temp dir, with an absolute path in the command
# (this ensures that Python runs the tests against the installed wheel
# and not the repo code)
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir()
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
Path.cwd(),
test_cwd,
)
else:
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
(test_cwd / "test_fail.py").write_text(resources.TEST_FAIL_CWD_FILE.read_text())
test_command_prepared = prepare_command(
build_options.test_command,
project=Path(".").resolve(),
project=Path.cwd(),
package=options.globals.package_dir.resolve(),
wheel=repaired_wheel,
)
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir()
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (remove if already exists)
output_wheel = None
if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
moved_wheel = move_file(repaired_wheel, output_wheel)
if moved_wheel != output_wheel.resolve():
log.warning(
"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
)
built_wheels.append(output_wheel)
@@ -593,7 +628,7 @@ def build(options: Options, tmp_path: Path) -> None:
# don't want to abort a build because of that)
shutil.rmtree(identifier_tmp_dir, ignore_errors=True)
log.build_end()
log.build_end(output_wheel)
except subprocess.CalledProcessError as error:
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
raise errors.FatalError(msg) from error
+14 -34
View File
@@ -1,5 +1,3 @@
from __future__ import annotations
import ast
import configparser
import contextlib
@@ -16,32 +14,16 @@ def get_parent(node: ast.AST | None, depth: int = 1) -> ast.AST | None:
def is_main(parent: ast.AST | None) -> bool:
if parent is None:
return False
# This would be much nicer with 3.10's pattern matching!
if not isinstance(parent, ast.If):
return False
if not isinstance(parent.test, ast.Compare):
return False
try:
(op,) = parent.test.ops
(comp,) = parent.test.comparators
except ValueError:
return False
if not isinstance(op, ast.Eq):
return False
values = {comp, parent.test.left}
mains = {x for x in values if isinstance(x, ast.Constant) and x.value == "__main__"}
if len(mains) != 1:
return False
consts = {x for x in values if isinstance(x, ast.Name) and x.id == "__name__"}
return len(consts) == 1
match parent:
case ast.If(test=ast.Compare(left=left, ops=[ast.Eq()], comparators=[comp])):
values = {left, comp}
mains = {x for x in values if isinstance(x, ast.Constant) and x.value == "__main__"}
if len(mains) != 1:
return False
consts = {x for x in values if isinstance(x, ast.Name) and x.id == "__name__"}
return len(consts) == 1
case _:
return False
class Analyzer(ast.NodeVisitor):
@@ -67,12 +49,10 @@ class Analyzer(ast.NodeVisitor):
parent is not None and get_parent(parent) is None and is_main(get_parent(node, 3))
)
if (
node.arg == "python_requires"
and isinstance(node.value, ast.Constant)
and (unnested or name_main_unnested)
):
self.requires_python = node.value.value
match node:
case ast.keyword(arg="python_requires", value=ast.Constant(value=str(version))):
if unnested or name_main_unnested:
self.requires_python = version
def setup_py_python_requires(content: str) -> str | None:
+1
View File
@@ -0,0 +1 @@
import _cross_venv; _cross_venv.initialize()
+83
View File
@@ -0,0 +1,83 @@
# This module is copied into the site-packages directory of an Android build environment, and
# activated via a .pth file when we want the environment to simulate Android.
import os
import platform
import re
import sys
import sysconfig
from pathlib import Path
from typing import Any
def initialize() -> None:
if not (host_triplet := os.environ.get("CIBW_HOST_TRIPLET")):
return
# os ######################################################################
def cross_os_uname() -> os.uname_result:
return os.uname_result(
(
"Linux",
"localhost",
# The Linux kernel version and release are unlikely to be significant, but return
# realistic values anyway (from an API level 24 emulator).
"3.18.91+",
"#1 SMP PREEMPT Tue Jan 9 20:35:43 UTC 2018",
host_triplet.split("-")[0],
)
)
os.name = "posix"
os.uname = cross_os_uname
# platform ################################################################
#
# We can't determine the user-visible Android version number from the API level, so return a
# string which will work fine for display, but will fail to parse as a version number.
def cross_android_ver(*args: Any, **kwargs: Any) -> platform.AndroidVer:
return platform.AndroidVer(
release=f"API level {cross_getandroidapilevel()}",
api_level=cross_getandroidapilevel(),
manufacturer="Google",
model="sdk_gphone64",
device="emu64",
is_emulator=True,
)
# platform.uname, platform.system etc. are all implemented in terms of platform.android_ver.
platform.android_ver = cross_android_ver
# sys #####################################################################
def cross_getandroidapilevel() -> int:
api_level = sysconfig.get_config_var("ANDROID_API_LEVEL")
assert isinstance(api_level, int)
return api_level
# Some packages may recognize sys.cross_compiling from the crossenv tool.
sys.cross_compiling = True # type: ignore[attr-defined]
sys.getandroidapilevel = cross_getandroidapilevel # type: ignore[attr-defined]
sys.implementation._multiarch = host_triplet # type: ignore[attr-defined]
sys.platform = "android"
# Determine the abiflags from the sysconfigdata filename.
sysconfigdata_path = next(Path(__file__).parent.glob("_sysconfigdata_*.py"))
abiflags_match = re.match(r"_sysconfigdata_(.*?)_", sysconfigdata_path.name)
assert abiflags_match is not None
sys.abiflags = abiflags_match[1]
# sysconfig ###############################################################
#
# We don't change the actual sys.base_prefix and base_exec_prefix, because that
# could have unpredictable effects. Instead, we change the internal variables
# used to generate sysconfig.get_path("include").
exec_prefix = sysconfig.get_config_var("exec_prefix")
sysconfig._BASE_PREFIX = sysconfig._BASE_EXEC_PREFIX = exec_prefix # type: ignore[attr-defined]
# Reload the sysconfigdata file, generating its name from sys.abiflags,
# sys.platform, and sys.implementation._multiarch.
sysconfig._init_config_vars() # type: ignore[attr-defined]
# sysconfig.get_platform, which determines the wheel tag, is implemented in terms of
# sys.platform, sysconfig.get_config_var("ANDROID_API_LEVEL") (see localized_vars in
# android.py), and os.uname.
+92 -51
View File
@@ -1,7 +1,5 @@
[linux]
python_configurations = [
{ identifier = "cp36-manylinux_x86_64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -9,8 +7,8 @@ python_configurations = [
{ identifier = "cp312-manylinux_x86_64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-manylinux_i686", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_i686", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-manylinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -18,13 +16,13 @@ python_configurations = [
{ identifier = "cp312-manylinux_i686", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "pp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
{ identifier = "cp314-manylinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "pp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
{ identifier = "pp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
{ identifier = "pp311-manylinux_x86_64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
{ identifier = "cp36-manylinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "gp311_242-manylinux_x86_64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
{ identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -32,8 +30,8 @@ python_configurations = [
{ identifier = "cp312-manylinux_aarch64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-manylinux_ppc64le", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_ppc64le", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-manylinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -41,8 +39,8 @@ python_configurations = [
{ identifier = "cp312-manylinux_ppc64le", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-manylinux_s390x", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_s390x", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-manylinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -50,8 +48,8 @@ python_configurations = [
{ identifier = "cp312-manylinux_s390x", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-manylinux_armv7l", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_armv7l", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-manylinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_armv7l", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_armv7l", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_armv7l", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -59,18 +57,26 @@ python_configurations = [
{ identifier = "cp312-manylinux_armv7l", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "pp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
{ identifier = "cp314-manylinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-manylinux_riscv64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_riscv64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_riscv64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-manylinux_riscv64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-manylinux_riscv64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-manylinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-manylinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-manylinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-manylinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "pp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
{ identifier = "pp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
{ identifier = "pp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
{ identifier = "pp37-manylinux_i686", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
{ identifier = "gp311_242-manylinux_aarch64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
{ identifier = "pp38-manylinux_i686", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_i686", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
{ identifier = "pp310-manylinux_i686", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
{ identifier = "pp311-manylinux_i686", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
{ identifier = "cp36-musllinux_x86_64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_x86_64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-musllinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_x86_64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -78,8 +84,8 @@ python_configurations = [
{ identifier = "cp312-musllinux_x86_64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_x86_64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-musllinux_i686", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_i686", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_x86_64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -87,8 +93,8 @@ python_configurations = [
{ identifier = "cp312-musllinux_i686", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_i686", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-musllinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_i686", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -96,8 +102,8 @@ python_configurations = [
{ identifier = "cp312-musllinux_aarch64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_aarch64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-musllinux_ppc64le", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_ppc64le", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_aarch64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -105,8 +111,8 @@ python_configurations = [
{ identifier = "cp312-musllinux_ppc64le", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_ppc64le", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-musllinux_s390x", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_s390x", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_ppc64le", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -114,8 +120,8 @@ python_configurations = [
{ identifier = "cp312-musllinux_s390x", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_s390x", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp36-musllinux_armv7l", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-musllinux_armv7l", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp314-musllinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_s390x", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_armv7l", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_armv7l", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_armv7l", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -123,12 +129,21 @@ python_configurations = [
{ identifier = "cp312-musllinux_armv7l", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_armv7l", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-musllinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_armv7l", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
{ identifier = "cp38-musllinux_riscv64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-musllinux_riscv64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-musllinux_riscv64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
{ identifier = "cp311-musllinux_riscv64", version = "3.11", path_str = "/opt/python/cp311-cp311" },
{ identifier = "cp312-musllinux_riscv64", version = "3.12", path_str = "/opt/python/cp312-cp312" },
{ identifier = "cp313-musllinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313" },
{ identifier = "cp313t-musllinux_riscv64", version = "3.13", path_str = "/opt/python/cp313-cp313t" },
{ identifier = "cp314-musllinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314" },
{ identifier = "cp314t-musllinux_riscv64", version = "3.14", path_str = "/opt/python/cp314-cp314t" },
]
[macos]
python_configurations = [
{ identifier = "cp36-macosx_x86_64", version = "3.6", url = "https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg" },
{ identifier = "cp37-macosx_x86_64", version = "3.7", url = "https://www.python.org/ftp/python/3.7.9/python-3.7.9-macosx10.9.pkg" },
{ identifier = "cp38-macosx_x86_64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
{ identifier = "cp38-macosx_arm64", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
{ identifier = "cp38-macosx_universal2", version = "3.8", url = "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg" },
@@ -144,29 +159,32 @@ python_configurations = [
{ identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
{ identifier = "cp313-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.3/python-3.13.3-macos11.pkg" },
{ identifier = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.3/python-3.13.3-macos11.pkg" },
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.3/python-3.13.3-macos11.pkg" },
{ identifier = "cp313t-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.3/python-3.13.3-macos11.pkg" },
{ identifier = "cp313t-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.3/python-3.13.3-macos11.pkg" },
{ identifier = "cp313t-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.3/python-3.13.3-macos11.pkg" },
{ identifier = "pp37-macosx_x86_64", version = "3.7", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-osx64.tar.bz2" },
{ identifier = "cp313-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
{ identifier = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
{ identifier = "cp313t-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
{ identifier = "cp313t-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
{ identifier = "cp313t-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.5/python-3.13.5-macos11.pkg" },
{ identifier = "cp314-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
{ identifier = "cp314-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
{ identifier = "cp314-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
{ identifier = "cp314t-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
{ identifier = "cp314t-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
{ identifier = "cp314t-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.0/python-3.14.0rc1-macos11.pkg" },
{ identifier = "pp38-macosx_x86_64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.bz2" },
{ identifier = "pp38-macosx_arm64", version = "3.8", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.bz2" },
{ identifier = "pp39-macosx_x86_64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.bz2" },
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2" },
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_x86_64.tar.bz2" },
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2" },
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_x86_64.tar.bz2" },
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_arm64.tar.bz2" },
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-macos_x86_64.tar.bz2" },
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-macos_arm64.tar.bz2" },
{ identifier = "gp311_242-macosx_x86_64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-amd64.tar.gz" },
{ identifier = "gp311_242-macosx_arm64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-aarch64.tar.gz" },
]
[windows]
python_configurations = [
{ identifier = "cp36-win32", version = "3.6.8", arch = "32" },
{ identifier = "cp36-win_amd64", version = "3.6.8", arch = "64" },
{ identifier = "cp37-win32", version = "3.7.9", arch = "32" },
{ identifier = "cp37-win_amd64", version = "3.7.9", arch = "64" },
{ identifier = "cp38-win32", version = "3.8.10", arch = "32" },
{ identifier = "cp38-win_amd64", version = "3.8.10", arch = "64" },
{ identifier = "cp39-win32", version = "3.9.13", arch = "32" },
@@ -177,24 +195,47 @@ python_configurations = [
{ identifier = "cp311-win_amd64", version = "3.11.9", arch = "64" },
{ identifier = "cp312-win32", version = "3.12.10", arch = "32" },
{ identifier = "cp312-win_amd64", version = "3.12.10", arch = "64" },
{ identifier = "cp313-win32", version = "3.13.3", arch = "32" },
{ identifier = "cp313t-win32", version = "3.13.3", arch = "32" },
{ identifier = "cp313-win_amd64", version = "3.13.3", arch = "64" },
{ identifier = "cp313t-win_amd64", version = "3.13.3", arch = "64" },
{ identifier = "cp313-win32", version = "3.13.5", arch = "32" },
{ identifier = "cp313t-win32", version = "3.13.5", arch = "32" },
{ identifier = "cp313-win_amd64", version = "3.13.5", arch = "64" },
{ identifier = "cp313t-win_amd64", version = "3.13.5", arch = "64" },
{ identifier = "cp314-win32", version = "3.14.0-rc1", arch = "32" },
{ identifier = "cp314t-win32", version = "3.14.0-rc1", arch = "32" },
{ identifier = "cp314-win_amd64", version = "3.14.0-rc1", arch = "64" },
{ identifier = "cp314t-win_amd64", version = "3.14.0-rc1", arch = "64" },
{ identifier = "cp39-win_arm64", version = "3.9.10", arch = "ARM64" },
{ identifier = "cp310-win_arm64", version = "3.10.11", arch = "ARM64" },
{ identifier = "cp311-win_arm64", version = "3.11.9", arch = "ARM64" },
{ identifier = "cp312-win_arm64", version = "3.12.10", arch = "ARM64" },
{ identifier = "cp313-win_arm64", version = "3.13.3", arch = "ARM64" },
{ identifier = "cp313t-win_arm64", version = "3.13.3", arch = "ARM64" },
{ identifier = "pp37-win_amd64", version = "3.7", arch = "64", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.9-win64.zip" },
{ identifier = "cp313-win_arm64", version = "3.13.5", arch = "ARM64" },
{ identifier = "cp313t-win_arm64", version = "3.13.5", arch = "ARM64" },
{ identifier = "cp314-win_arm64", version = "3.14.0-rc1", arch = "ARM64" },
{ identifier = "cp314t-win_arm64", version = "3.14.0-rc1", arch = "ARM64" },
{ identifier = "pp38-win_amd64", version = "3.8", arch = "64", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip" },
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip" },
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-win64.zip" },
{ identifier = "pp311-win_amd64", version = "3.11", arch = "64", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-win64.zip" },
{ identifier = "pp311-win_amd64", version = "3.11", arch = "64", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.20-win64.zip" },
{ identifier = "gp311_242-win_amd64", version = "3.11", arch = "64", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-windows-amd64.zip" },
]
[pyodide]
python_configurations = [
{ identifier = "cp312-pyodide_wasm32", version = "3.12", pyodide_version = "0.27.0", pyodide_build_version = "0.29.2", emscripten_version = "3.1.58", node_version = "v20" },
{ identifier = "cp312-pyodide_wasm32", version = "3.12", default_pyodide_version = "0.27.7", node_version = "v22" },
{ identifier = "cp313-pyodide_wasm32", version = "3.13", default_pyodide_version = "0.28.0", node_version = "v22" },
]
[android]
python_configurations = [
{ identifier = "cp313-android_arm64_v8a", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.5+20250722.214220/python-3.13.5+20250722.214220-aarch64-linux-android.tar.gz" },
{ identifier = "cp313-android_x86_64", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.5+20250722.214220/python-3.13.5+20250722.214220-x86_64-linux-android.tar.gz" },
]
[ios]
python_configurations = [
{ identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b9/Python-3.13-iOS-support.b9.tar.gz" },
{ identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b9/Python-3.13-iOS-support.b9.tar.gz" },
{ identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b9/Python-3.13-iOS-support.b9.tar.gz" },
{ identifier = "cp314-ios_arm64_iphoneos", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b5/Python-3.14-iOS-support.b5.tar.gz" },
{ identifier = "cp314-ios_x86_64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b5/Python-3.14-iOS-support.b5.tar.gz" },
{ identifier = "cp314-ios_arm64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b5/Python-3.14-iOS-support.b5.tar.gz" },
]
+310 -11
View File
@@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json",
"$defs": {
"inherit": {
@@ -13,9 +13,13 @@
},
"enable": {
"enum": [
"cpython-experimental-riscv64",
"cpython-freethreading",
"cpython-prerelease",
"pypy"
"graalpy",
"pyodide-prerelease",
"pypy",
"pypy-eol"
]
},
"description": "A Python version or flavor to enable."
@@ -104,7 +108,7 @@
},
"build-frontend": {
"default": "default",
"description": "Set the tool to use to build, either \"pip\" (default for now), \"build\", or \"build[uv]\"",
"description": "Set the tool to use to build, either \"build\" (default), \"build[uv]\", or \"pip\"",
"oneOf": [
{
"enum": [
@@ -233,7 +237,45 @@
"dependency-versions": {
"default": "pinned",
"description": "Specify how cibuildwheel controls the versions of the tools it uses",
"type": "string",
"oneOf": [
{
"enum": [
"pinned",
"latest"
]
},
{
"type": "string",
"description": "Path to a file containing dependency versions, or inline package specifications, starting with \"packages:\"",
"not": {
"enum": [
"pinned",
"latest"
]
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"file": {
"type": "string"
}
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"packages": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
],
"title": "CIBW_DEPENDENCY_VERSIONS"
},
"enable": {
@@ -284,13 +326,6 @@
],
"title": "CIBW_ENVIRONMENT_PASS"
},
"free-threaded-support": {
"type": "boolean",
"default": false,
"description": "The project supports free-threaded builds of Python (PEP703)",
"deprecated": "Use the `enable` option instead.",
"title": "CIBW_FREE_THREADED_SUPPORT"
},
"manylinux-aarch64-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
@@ -326,6 +361,11 @@
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MANYLINUX_PYPY_X86_64_IMAGE"
},
"manylinux-riscv64-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MANYLINUX_RISCV64_IMAGE"
},
"manylinux-s390x-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
@@ -356,6 +396,11 @@
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_PPC64LE_IMAGE"
},
"musllinux-riscv64-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_RISCV64_IMAGE"
},
"musllinux-s390x-image": {
"type": "string",
"description": "Specify alternative manylinux / musllinux container images",
@@ -366,6 +411,26 @@
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_X86_64_IMAGE"
},
"xbuild-tools": {
"description": "Binaries on the path that should be included in an isolated cross-build environment",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"title": "CIBW_XBUILD_TOOLS"
},
"pyodide-version": {
"type": "string",
"description": "Specify the version of Pyodide to use",
"title": "CIBW_PYODIDE_VERSION"
},
"repair-wheel-command": {
"description": "Execute a shell command to repair each built wheel.",
"oneOf": [
@@ -426,6 +491,21 @@
],
"title": "CIBW_TEST_EXTRAS"
},
"test-sources": {
"description": "Test files that are required by the test environment",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"title": "CIBW_TEST_SOURCES"
},
"test-groups": {
"description": "Install extra groups when testing",
"oneOf": [
@@ -471,6 +551,24 @@
],
"title": "CIBW_TEST_SKIP"
},
"test-environment": {
"description": "Set environment variables for the test environment",
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"additionalProperties": false,
"patternProperties": {
".+": {
"type": "string"
}
}
}
],
"title": "CIBW_TEST_ENVIRONMENT"
},
"overrides": {
"type": "array",
"description": "An overrides array",
@@ -505,6 +603,9 @@
"before-build": {
"$ref": "#/$defs/inherit"
},
"xbuild-tools": {
"$ref": "#/$defs/inherit"
},
"before-test": {
"$ref": "#/$defs/inherit"
},
@@ -529,8 +630,14 @@
"test-extras": {
"$ref": "#/$defs/inherit"
},
"test-sources": {
"$ref": "#/$defs/inherit"
},
"test-requires": {
"$ref": "#/$defs/inherit"
},
"test-environment": {
"$ref": "#/$defs/inherit"
}
}
},
@@ -585,6 +692,9 @@
"manylinux-pypy_x86_64-image": {
"$ref": "#/properties/manylinux-pypy_x86_64-image"
},
"manylinux-riscv64-image": {
"$ref": "#/properties/manylinux-riscv64-image"
},
"manylinux-s390x-image": {
"$ref": "#/properties/manylinux-s390x-image"
},
@@ -603,12 +713,21 @@
"musllinux-ppc64le-image": {
"$ref": "#/properties/musllinux-ppc64le-image"
},
"musllinux-riscv64-image": {
"$ref": "#/properties/musllinux-riscv64-image"
},
"musllinux-s390x-image": {
"$ref": "#/properties/musllinux-s390x-image"
},
"musllinux-x86_64-image": {
"$ref": "#/properties/musllinux-x86_64-image"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
@@ -618,11 +737,17 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
}
}
}
@@ -682,6 +807,9 @@
"manylinux-pypy_x86_64-image": {
"$ref": "#/properties/manylinux-pypy_x86_64-image"
},
"manylinux-riscv64-image": {
"$ref": "#/properties/manylinux-riscv64-image"
},
"manylinux-s390x-image": {
"$ref": "#/properties/manylinux-s390x-image"
},
@@ -700,12 +828,21 @@
"musllinux-ppc64le-image": {
"$ref": "#/properties/musllinux-ppc64le-image"
},
"musllinux-riscv64-image": {
"$ref": "#/properties/musllinux-riscv64-image"
},
"musllinux-s390x-image": {
"$ref": "#/properties/musllinux-s390x-image"
},
"musllinux-x86_64-image": {
"$ref": "#/properties/musllinux-x86_64-image"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"description": "Execute a shell command to repair each built wheel.",
"oneOf": [
@@ -728,11 +865,17 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
}
}
},
@@ -767,6 +910,12 @@
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
@@ -776,11 +925,17 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
}
}
},
@@ -815,6 +970,12 @@
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"description": "Execute a shell command to repair each built wheel.",
"oneOf": [
@@ -837,11 +998,17 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
}
}
},
@@ -876,6 +1043,12 @@
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
@@ -885,11 +1058,137 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
}
}
},
"android": {
"type": "object",
"additionalProperties": false,
"properties": {
"archs": {
"$ref": "#/properties/archs"
},
"before-all": {
"$ref": "#/properties/before-all"
},
"before-build": {
"$ref": "#/properties/before-build"
},
"before-test": {
"$ref": "#/properties/before-test"
},
"build-frontend": {
"$ref": "#/properties/build-frontend"
},
"build-verbosity": {
"$ref": "#/properties/build-verbosity"
},
"config-settings": {
"$ref": "#/properties/config-settings"
},
"dependency-versions": {
"$ref": "#/properties/dependency-versions"
},
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
"test-command": {
"$ref": "#/properties/test-command"
},
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
}
}
},
"ios": {
"type": "object",
"additionalProperties": false,
"properties": {
"archs": {
"$ref": "#/properties/archs"
},
"before-all": {
"$ref": "#/properties/before-all"
},
"before-build": {
"$ref": "#/properties/before-build"
},
"before-test": {
"$ref": "#/properties/before-test"
},
"build-frontend": {
"$ref": "#/properties/build-frontend"
},
"build-verbosity": {
"$ref": "#/properties/build-verbosity"
},
"config-settings": {
"$ref": "#/properties/config-settings"
},
"dependency-versions": {
"$ref": "#/properties/dependency-versions"
},
"environment": {
"$ref": "#/properties/environment"
},
"xbuild-tools": {
"$ref": "#/properties/xbuild-tools"
},
"pyodide-version": {
"$ref": "#/properties/pyodide-version"
},
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
"test-command": {
"$ref": "#/properties/test-command"
},
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
},
"test-environment": {
"$ref": "#/properties/test-environment"
}
}
}
@@ -4,30 +4,30 @@ annotated-types==0.7.0
# via pydantic
anyio==4.9.0
# via httpx
auditwheel-emscripten==0.0.16
auditwheel-emscripten==0.1.0
# via pyodide-build
build==1.2.2.post1
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# pyodide-build
certifi==2025.1.31
certifi==2025.7.14
# via
# httpcore
# httpx
# requests
charset-normalizer==3.4.1
charset-normalizer==3.4.2
# via requests
click==8.1.8
# via typer
cmake==4.0.0
# via pyodide-build
distlib==0.3.9
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# typer
distlib==0.4.0
# via virtualenv
filelock==3.18.0
# via virtualenv
h11==0.14.0
h11==0.16.0
# via httpcore
httpcore==1.0.8
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via unearth
@@ -48,21 +48,23 @@ packaging==25.0
# build
# pyodide-build
# unearth
pip==25.0.1
pip==25.1.1
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
platformdirs==4.3.7
# via virtualenv
pydantic==2.11.3
platformdirs==4.3.8
# via
# pyodide-build
# virtualenv
pydantic==2.11.7
# via
# pyodide-build
# pyodide-lock
pydantic-core==2.33.1
pydantic-core==2.33.2
# via pydantic
pygments==2.19.1
pygments==2.19.2
# via rich
pyodide-build==0.29.2
pyodide-build==0.30.5
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.2.4
pyodide-cli==0.3.0
# via
# auditwheel-emscripten
# pyodide-build
@@ -70,16 +72,16 @@ pyodide-lock==0.1.0a7
# via pyodide-build
pyproject-hooks==1.2.0
# via build
requests==2.32.3
requests==2.32.4
# via pyodide-build
resolvelib==1.1.0
resolvelib==1.2.0
# via pyodide-build
rich==14.0.0
# via
# pyodide-build
# pyodide-cli
# typer
ruamel-yaml==0.18.10
ruamel-yaml==0.18.14
# via pyodide-build
ruamel-yaml-clib==0.2.12
# via ruamel-yaml
@@ -87,25 +89,25 @@ shellingham==1.5.4
# via typer
sniffio==1.3.1
# via anyio
typer==0.15.2
typer==0.16.0
# via
# auditwheel-emscripten
# pyodide-build
# pyodide-cli
typing-extensions==4.13.2
typing-extensions==4.14.1
# via
# anyio
# pydantic
# pydantic-core
# typer
# typing-inspection
typing-inspection==0.4.0
typing-inspection==0.4.1
# via pydantic
unearth==0.17.5
# via pyodide-build
urllib3==2.4.0
urllib3==2.5.0
# via requests
virtualenv==20.30.0
virtualenv==20.32.0
# via
# build
# pyodide-build
@@ -0,0 +1,116 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
annotated-types==0.7.0
# via pydantic
anyio==4.9.0
# via httpx
auditwheel-emscripten==0.1.0
# via pyodide-build
build==1.2.2.post1
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# pyodide-build
certifi==2025.7.14
# via
# httpcore
# httpx
# requests
charset-normalizer==3.4.2
# via requests
click==8.1.8
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# typer
distlib==0.4.0
# via virtualenv
filelock==3.18.0
# via virtualenv
h11==0.16.0
# via httpcore
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via unearth
idna==3.10
# via
# anyio
# httpx
# requests
leb128==1.0.8
# via auditwheel-emscripten
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
packaging==25.0
# via
# auditwheel-emscripten
# build
# pyodide-build
# unearth
pip==25.1.1
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
platformdirs==4.3.8
# via
# pyodide-build
# virtualenv
pydantic==2.11.7
# via
# pyodide-build
# pyodide-lock
pydantic-core==2.33.2
# via pydantic
pygments==2.19.2
# via rich
pyodide-build==0.30.5
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.3.0
# via
# auditwheel-emscripten
# pyodide-build
pyodide-lock==0.1.0a7
# via pyodide-build
pyproject-hooks==1.2.0
# via build
requests==2.32.4
# via pyodide-build
resolvelib==1.2.0
# via pyodide-build
rich==14.0.0
# via
# pyodide-build
# pyodide-cli
# typer
ruamel-yaml==0.18.14
# via pyodide-build
ruamel-yaml-clib==0.2.12
# via ruamel-yaml
shellingham==1.5.4
# via typer
sniffio==1.3.1
# via anyio
typer==0.16.0
# via
# auditwheel-emscripten
# pyodide-build
# pyodide-cli
typing-extensions==4.14.1
# via
# pydantic
# pydantic-core
# typer
# typing-inspection
typing-inspection==0.4.1
# via pydantic
unearth==0.17.5
# via pyodide-build
urllib3==2.5.0
# via requests
virtualenv==20.32.0
# via
# build
# pyodide-build
wheel==0.45.1
# via
# auditwheel-emscripten
# pyodide-build
@@ -6,11 +6,11 @@ build==1.2.2.post1
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
filelock==3.18.0
# via virtualenv
importlib-metadata==8.6.1
importlib-metadata==8.7.0
# via build
macholib==1.16.3
# via delocate
@@ -18,17 +18,17 @@ packaging==25.0
# via
# build
# delocate
pip==25.0.1
pip==25.1.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.3.7
platformdirs==4.3.8
# via virtualenv
pyproject-hooks==1.2.0
# via build
tomli==2.2.1
# via build
typing-extensions==4.13.2
typing-extensions==4.14.1
# via delocate
virtualenv==20.30.0
virtualenv==20.32.0
# via -r cibuildwheel/resources/constraints.in
zipp==3.21.0
zipp==3.23.0
# via importlib-metadata
@@ -6,7 +6,7 @@ build==1.2.2.post1
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
filelock==3.18.0
# via virtualenv
@@ -16,13 +16,13 @@ packaging==25.0
# via
# build
# delocate
pip==25.0.1
pip==25.1.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.3.7
platformdirs==4.3.8
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.13.2
typing-extensions==4.14.1
# via delocate
virtualenv==20.30.0
virtualenv==20.32.0
# via -r cibuildwheel/resources/constraints.in
@@ -6,7 +6,7 @@ build==1.2.2.post1
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
filelock==3.18.0
# via virtualenv
@@ -16,13 +16,13 @@ packaging==25.0
# via
# build
# delocate
pip==25.0.1
pip==25.1.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.3.7
platformdirs==4.3.8
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.13.2
typing-extensions==4.14.1
# via delocate
virtualenv==20.30.0
virtualenv==20.32.0
# via -r cibuildwheel/resources/constraints.in
@@ -6,7 +6,7 @@ build==1.2.2.post1
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
filelock==3.18.0
# via virtualenv
@@ -16,13 +16,13 @@ packaging==25.0
# via
# build
# delocate
pip==25.0.1
pip==25.1.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.3.7
platformdirs==4.3.8
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.13.2
typing-extensions==4.14.1
# via delocate
virtualenv==20.30.0
virtualenv==20.32.0
# via -r cibuildwheel/resources/constraints.in
@@ -2,38 +2,27 @@
# nox -s update_constraints
altgraph==0.17.4
# via macholib
build==1.1.1
build==1.2.2.post1
# via -r cibuildwheel/resources/constraints.in
delocate==0.12.0
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
filelock==3.12.2
filelock==3.18.0
# via virtualenv
importlib-metadata==6.7.0
# via
# build
# virtualenv
macholib==1.16.3
# via delocate
packaging==24.0
packaging==25.0
# via
# build
# delocate
pip==24.0
pip==25.1.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.0.0
platformdirs==4.3.8
# via virtualenv
pyproject-hooks==1.2.0
# via build
tomli==2.0.1
# via build
typing-extensions==4.7.1
# via
# delocate
# importlib-metadata
# platformdirs
virtualenv==20.26.6
typing-extensions==4.14.1
# via delocate
virtualenv==20.32.0
# via -r cibuildwheel/resources/constraints.in
zipp==3.15.0
# via importlib-metadata
@@ -1,52 +0,0 @@
#
# This file is autogenerated by pip-compile with python 3.6
# To update, run:
#
# nox -s update_constraints-3.6
#
build==0.9.0
# via -r cibuildwheel/resources/constraints.in
delocate==0.10.2
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.8
# via virtualenv
filelock==3.4.1
# via virtualenv
importlib-metadata==4.8.3
# via
# build
# pep517
# virtualenv
importlib-resources==5.4.0
# via virtualenv
packaging==21.3
# via build
pep517==0.13.1
# via build
platformdirs==2.4.0
# via virtualenv
pyparsing==3.1.1
# via packaging
tomli==1.2.3
# via
# build
# pep517
typing-extensions==4.1.1
# via
# delocate
# importlib-metadata
virtualenv==20.17.1
# via -r cibuildwheel/resources/constraints.in
wheel==0.37.1
# via delocate
zipp==3.6.0
# via
# importlib-metadata
# importlib-resources
# pep517
# The following packages are considered to be unsafe in a requirements file:
pip==21.3.1
# via -r cibuildwheel/resources/constraints.in
setuptools==59.6.0
# via -r cibuildwheel/resources/constraints.in
@@ -6,7 +6,7 @@ build==1.2.2.post1
# via -r cibuildwheel/resources/constraints.in
delocate==0.12.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
filelock==3.16.1
# via virtualenv
@@ -28,7 +28,7 @@ tomli==2.2.1
# via build
typing-extensions==4.13.2
# via delocate
virtualenv==20.30.0
virtualenv==20.32.0
# via -r cibuildwheel/resources/constraints.in
zipp==3.20.2
# via importlib-metadata
@@ -6,11 +6,11 @@ build==1.2.2.post1
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
filelock==3.18.0
# via virtualenv
importlib-metadata==8.6.1
importlib-metadata==8.7.0
# via build
macholib==1.16.3
# via delocate
@@ -18,17 +18,17 @@ packaging==25.0
# via
# build
# delocate
pip==25.0.1
pip==25.1.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.3.7
platformdirs==4.3.8
# via virtualenv
pyproject-hooks==1.2.0
# via build
tomli==2.2.1
# via build
typing-extensions==4.13.2
typing-extensions==4.14.1
# via delocate
virtualenv==20.30.0
virtualenv==20.32.0
# via -r cibuildwheel/resources/constraints.in
zipp==3.21.0
zipp==3.23.0
# via importlib-metadata
+5 -5
View File
@@ -6,7 +6,7 @@ build==1.2.2.post1
# via -r cibuildwheel/resources/constraints.in
delocate==0.13.0
# via -r cibuildwheel/resources/constraints.in
distlib==0.3.9
distlib==0.4.0
# via virtualenv
filelock==3.18.0
# via virtualenv
@@ -16,13 +16,13 @@ packaging==25.0
# via
# build
# delocate
pip==25.0.1
pip==25.1.1
# via -r cibuildwheel/resources/constraints.in
platformdirs==4.3.7
platformdirs==4.3.8
# via virtualenv
pyproject-hooks==1.2.0
# via build
typing-extensions==4.13.2
typing-extensions==4.14.1
# via delocate
virtualenv==20.30.0
virtualenv==20.32.0
# via -r cibuildwheel/resources/constraints.in
+20 -9
View File
@@ -2,7 +2,6 @@
build = "*"
skip = ""
test-skip = ""
free-threaded-support = false
enable = []
archs = ["auto"]
@@ -15,25 +14,32 @@ build-verbosity = 0
before-all = ""
before-build = ""
# TOML doesn't support explicit NULLs; use ["\u0000"] as a sentinel value.
xbuild-tools = ["\u0000"]
repair-wheel-command = ""
test-command = ""
before-test = ""
test-sources = []
test-requires = []
test-extras = []
test-groups = []
test-environment = {}
container-engine = "docker"
manylinux-x86_64-image = "manylinux2014"
manylinux-i686-image = "manylinux2014"
manylinux-aarch64-image = "manylinux2014"
manylinux-ppc64le-image = "manylinux2014"
manylinux-s390x-image = "manylinux2014"
pyodide-version = ""
manylinux-x86_64-image = "manylinux_2_28"
manylinux-i686-image = "manylinux_2_28"
manylinux-aarch64-image = "manylinux_2_28"
manylinux-ppc64le-image = "manylinux_2_28"
manylinux-s390x-image = "manylinux_2_28"
manylinux-armv7l-image = "manylinux_2_31"
manylinux-pypy_x86_64-image = "manylinux2014"
manylinux-pypy_i686-image = "manylinux2014"
manylinux-pypy_aarch64-image = "manylinux2014"
manylinux-riscv64-image = "manylinux_2_39"
manylinux-pypy_x86_64-image = "manylinux_2_28"
manylinux-pypy_i686-image = "manylinux_2_28"
manylinux-pypy_aarch64-image = "manylinux_2_28"
musllinux-x86_64-image = "musllinux_1_2"
musllinux-i686-image = "musllinux_1_2"
@@ -41,6 +47,7 @@ musllinux-aarch64-image = "musllinux_1_2"
musllinux-ppc64le-image = "musllinux_1_2"
musllinux-s390x-image = "musllinux_1_2"
musllinux-armv7l-image = "musllinux_1_2"
musllinux-riscv64-image = "musllinux_1_2"
[tool.cibuildwheel.linux]
@@ -51,4 +58,8 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest
[tool.cibuildwheel.windows]
[tool.cibuildwheel.android]
[tool.cibuildwheel.ios]
[tool.cibuildwheel.pyodide]
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>attributeSetting</key>
<integer>1</integer>
<key>choiceAttribute</key>
<string>selected</string>
<key>choiceIdentifier</key>
<string>org.python.Python.PythonTFramework-3.14</string>
</dict>
</array>
</plist>
+2 -2
View File
@@ -26,14 +26,14 @@ STAT_0o775 = (
)
def main():
def main() -> None:
openssl_dir, openssl_cafile = os.path.split(ssl.get_default_verify_paths().openssl_cafile)
print(" -- pip install --upgrade certifi")
subprocess.check_call(
[sys.executable, "-E", "-s", "-m", "pip", "install", "--upgrade", "certifi"]
)
import certifi
import certifi # noqa: PLC0415
# change working directory to the default SSL directory
os.chdir(openssl_dir)
+2 -2
View File
@@ -1,3 +1,3 @@
url = "https://nodejs.org/dist/"
v22 = "v22.14.0"
v20 = "v20.19.0"
v22 = "v22.17.1"
v20 = "v20.19.4"
+39 -44
View File
@@ -1,58 +1,53 @@
[x86_64]
manylinux1 = quay.io/pypa/manylinux1_x86_64:2024-04-29-76807b8
manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2025.04.19-1
manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2025.04.19-1
musllinux_1_1 = quay.io/pypa/musllinux_1_1_x86_64:2024.10.26-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2025.04.19-1
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2025.07.23-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2025.07.23-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2025.07.23-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2025.07.23-1
[i686]
manylinux1 = quay.io/pypa/manylinux1_i686:2024-04-29-76807b8
manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
manylinux2014 = quay.io/pypa/manylinux2014_i686:2025.04.19-1
manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-12-26-0d38463
musllinux_1_1 = quay.io/pypa/musllinux_1_1_i686:2024.10.26-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2025.04.19-1
[pypy_x86_64]
manylinux2010 = quay.io/pypa/manylinux2010_x86_64:2022-08-05-4535177
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2025.04.19-1
manylinux_2_24 = quay.io/pypa/manylinux_2_24_x86_64:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2025.04.19-1
[pypy_i686]
manylinux2010 = quay.io/pypa/manylinux2010_i686:2022-08-05-4535177
manylinux2014 = quay.io/pypa/manylinux2014_i686:2025.04.19-1
manylinux_2_24 = quay.io/pypa/manylinux_2_24_i686:2022-12-26-0d38463
manylinux2014 = quay.io/pypa/manylinux2014_i686:2025.07.23-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2025.07.23-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2025.07.23-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_i686:2025.07.23-1
[aarch64]
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2025.04.19-1
manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2025.04.19-1
musllinux_1_1 = quay.io/pypa/musllinux_1_1_aarch64:2024.10.26-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2025.04.19-1
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2025.07.23-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2025.07.23-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2025.07.23-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2025.07.23-1
[ppc64le]
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2025.04.19-1
manylinux_2_24 = quay.io/pypa/manylinux_2_24_ppc64le:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2025.04.19-1
musllinux_1_1 = quay.io/pypa/musllinux_1_1_ppc64le:2024.10.26-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2025.04.19-1
manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2025.07.23-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_ppc64le:2025.07.23-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_ppc64le:2025.07.23-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_ppc64le:2025.07.23-1
[s390x]
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2025.04.19-1
manylinux_2_24 = quay.io/pypa/manylinux_2_24_s390x:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x:2025.04.19-1
musllinux_1_1 = quay.io/pypa/musllinux_1_1_s390x:2024.10.26-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2025.04.19-1
manylinux2014 = quay.io/pypa/manylinux2014_s390x:2025.07.23-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_s390x:2025.07.23-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_s390x:2025.07.23-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_s390x:2025.07.23-1
[pypy_x86_64]
manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2025.07.23-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2025.07.23-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2025.07.23-1
[pypy_i686]
manylinux2014 = quay.io/pypa/manylinux2014_i686:2025.07.23-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_i686:2025.07.23-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_i686:2025.07.23-1
[pypy_aarch64]
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2025.04.19-1
manylinux_2_24 = quay.io/pypa/manylinux_2_24_aarch64:2022-12-26-0d38463
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2025.04.19-1
manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2025.07.23-1
manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2025.07.23-1
manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2025.07.23-1
[armv7l]
manylinux_2_31 = quay.io/pypa/manylinux_2_31_armv7l:2025.04.19-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_armv7l:2025.04.19-1
manylinux_2_31 = quay.io/pypa/manylinux_2_31_armv7l:2025.07.23-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_armv7l:2025.07.23-1
[riscv64]
manylinux_2_39 = quay.io/pypa/manylinux_2_39_riscv64:2025.07.23-1
musllinux_1_2 = quay.io/pypa/musllinux_1_2_riscv64:2025.07.23-1
@@ -0,0 +1,457 @@
{
"releases": [
{
"tag": "20250712",
"assets": [
{
"name": "cpython-3.10.18+20250712-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.10.18+20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.10.18%2B20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.11.13+20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.11.13%2B20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.12.11+20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.12.11%2B20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.13.5+20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.13.5%2B20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-aarch64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-aarch64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.14.0b4+20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.14.0b4%2B20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-aarch64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-aarch64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-aarch64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-armv7-unknown-linux-gnueabi-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-armv7-unknown-linux-gnueabihf-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-i686-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-i686-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-ppc64le-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-riscv64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-riscv64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-s390x-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-s390x-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64-apple-darwin-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64-apple-darwin-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64-pc-windows-msvc-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64-pc-windows-msvc-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64_v2-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64_v2-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64_v3-unknown-linux-musl-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64_v4-unknown-linux-gnu-install_only.tar.gz"
},
{
"name": "cpython-3.9.23+20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20250712/cpython-3.9.23%2B20250712-x86_64_v4-unknown-linux-musl-install_only.tar.gz"
}
]
}
]
}
+29 -10
View File
@@ -1,17 +1,36 @@
# this file is copied to the testing cwd, to raise the below error message if
# pytest/unittest is run from there.
import sys
import unittest
from typing import NoReturn
class TestStringMethods(unittest.TestCase):
def test_fail(self):
self.fail(
"cibuildwheel executes tests from a different working directory to "
"your project. This ensures only your wheel is imported, preventing "
"Python from accessing files that haven't been packaged into the "
"wheel. Please specify a path to your tests when invoking pytest "
"using the {project} placeholder, e.g. `pytest {project}` or "
"`pytest {project}/tests`. cibuildwheel will replace {project} with "
"the path to your project."
)
def test_fail(self) -> NoReturn:
if sys.platform in ["android", "ios"]:
msg = (
"You tried to run tests from the testbed app's working "
"directory, without specifying `test-sources`. "
"On this platform, you must copy your test files to the testbed app by "
"setting the `test-sources` option in your cibuildwheel "
"configuration."
)
else:
msg = (
"cibuildwheel executes tests from a different working directory to "
"your project. This ensures only your wheel is imported, preventing "
"Python from accessing files that haven't been packaged into the "
"wheel. "
"\n\n"
"Please specify a path to your tests when invoking pytest "
"using the {project} placeholder, e.g. `pytest {project}` or "
"`pytest {project}/tests`. cibuildwheel will replace {project} with "
"the path to your project. "
"\n\n"
"Alternatively, you can specify your test files using the "
"`test-sources` option, and cibuildwheel will copy them to the "
"working directory for testing."
)
self.fail(msg)
+1 -2
View File
@@ -1,2 +1 @@
py36 = { version = "20.17.1", url = "https://bootstrap.pypa.io/virtualenv/3.6/virtualenv.pyz" }
default = { version = "20.26.6", url = "https://bootstrap.pypa.io/virtualenv/3.7/virtualenv.pyz" }
default = { version = "20.32.0", url = "https://github.com/pypa/get-virtualenv/blob/20.32.0/public/virtualenv.pyz?raw=true" }
+2 -5
View File
@@ -1,15 +1,12 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
DIR = Path(__file__).parent.resolve()
from .util import resources
def get_schema(tool_name: str = "cibuildwheel") -> dict[str, Any]:
"Get the stored complete schema for cibuildwheel settings."
assert tool_name == "cibuildwheel", "Only cibuildwheel is supported."
with DIR.joinpath("resources/cibuildwheel.schema.json").open(encoding="utf-8") as f:
with resources.CIBUILDWHEEL_SCHEMA.open(encoding="utf-8") as f:
return json.load(f) # type: ignore[no-any-return]
+141
View File
@@ -0,0 +1,141 @@
import dataclasses
import itertools
from enum import StrEnum
from fnmatch import fnmatch
from typing import Any
import bracex
from packaging.specifiers import SpecifierSet
from packaging.version import Version
def selector_matches(patterns: str, string: str) -> bool:
"""
Returns True if `string` is matched by any of the wildcard patterns in
`patterns`.
Matching is according to fnmatch, but with shell-like curly brace
expansion. For example, 'cp{36,37}-*' would match either of 'cp36-*' or
'cp37-*'.
"""
patterns_list = patterns.split()
expanded_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in patterns_list)
return any(fnmatch(string, pat) for pat in expanded_patterns)
class EnableGroup(StrEnum):
"""
Groups of build selectors that are not enabled by default.
"""
CPythonExperimentalRiscV64 = "cpython-experimental-riscv64"
CPythonFreeThreading = "cpython-freethreading"
CPythonPrerelease = "cpython-prerelease"
GraalPy = "graalpy"
PyPy = "pypy"
PyPyEoL = "pypy-eol"
PyodidePrerelease = "pyodide-prerelease"
@classmethod
def all_groups(cls) -> frozenset["EnableGroup"]:
return frozenset(cls)
@classmethod
def parse_option_value(cls, value: str) -> frozenset["EnableGroup"]:
"""
Parses a string of space-separated values into a set of EnableGroup
members. The string may contain group names or "all".
"""
result = set()
for group in value.strip().split():
if group == "all":
return cls.all_groups()
try:
result.add(cls(group))
except ValueError:
msg = f"Unknown enable group: {group}"
raise ValueError(msg) from None
return frozenset(result)
@dataclasses.dataclass(frozen=True, kw_only=True)
class BuildSelector:
"""
This class holds a set of build/skip patterns. You call an instance with a
build identifier, and it returns True if that identifier should be
included. Only call this on valid identifiers, ones that have at least 2
numeric digits before the first dash.
"""
build_config: str
skip_config: str
requires_python: SpecifierSet | None = None
enable: frozenset[EnableGroup] = frozenset()
def __call__(self, build_id: str) -> bool:
# Filter build selectors by python_requires if set
if self.requires_python is not None:
py_ver_str = build_id.split("-")[0].split("_")[0]
py_ver_str = py_ver_str.removesuffix("t")
major = int(py_ver_str[2])
minor = int(py_ver_str[3:])
version = Version(f"{major}.{minor}.99")
if not self.requires_python.contains(version):
return False
# filter out groups that are not enabled
if EnableGroup.CPythonFreeThreading not in self.enable and fnmatch(build_id, "cp313t-*"):
return False
if EnableGroup.CPythonPrerelease not in self.enable and fnmatch(build_id, "cp315*"):
return False
if EnableGroup.PyPy not in self.enable and fnmatch(build_id, "pp31*"):
return False
if EnableGroup.PyPyEoL not in self.enable and fnmatch(build_id, "pp3?-*"):
return False
if EnableGroup.CPythonExperimentalRiscV64 not in self.enable and fnmatch(
build_id, "*_riscv64"
):
return False
if EnableGroup.GraalPy not in self.enable and fnmatch(build_id, "gp*"):
return False
# TODO: Re-enable this when we have Pyodide prereleases again (e.g., 0.29.0a1+)
# Python 3.13 support became stable in Pyodide 0.28.0, so it no longer needs a prerelease
# flag.
# Also update Pyodide tests in unit_test/build_selector_test.py accordingly.
# When re-enabling, update the pattern to match the experimental Python version in case
# it is bumped to Python 3.14 (likely cp314-pyodide_* but could remain as 3.13 as well).
# This depends on the CPython version being used in the Pyodide runtime at the time.
# if EnableGroup.PyodidePrerelease not in self.enable and fnmatch(
# build_id, "cp313-pyodide_*"
# ):
# return False
should_build = selector_matches(self.build_config, build_id)
should_skip = selector_matches(self.skip_config, build_id)
return should_build and not should_skip
def options_summary(self) -> Any:
return {
"build_config": self.build_config,
"skip_config": self.skip_config,
"requires_python": str(self.requires_python),
"enable": sorted(group.value for group in self.enable),
}
@dataclasses.dataclass(frozen=True, kw_only=True)
class TestSelector:
"""
A build selector that can only skip tests according to a skip pattern.
"""
skip_config: str
def __call__(self, build_id: str) -> bool:
should_skip = selector_matches(self.skip_config, build_id)
return not should_skip
def options_summary(self) -> Any:
return {"skip_config": self.skip_config}
+4 -13
View File
@@ -1,28 +1,19 @@
from __future__ import annotations
import os
import subprocess
import typing
from typing import Final, Literal, Protocol, Union
from typing import Final, Literal, Protocol
__all__ = (
"PLATFORMS",
"PathOrStr",
"PlatformName",
"PopenBytes",
)
if typing.TYPE_CHECKING:
PopenBytes = subprocess.Popen[bytes]
PathOrStr = Union[str, os.PathLike[str]]
else:
PopenBytes = subprocess.Popen
PathOrStr = Union[str, "os.PathLike[str]"]
PathOrStr = str | os.PathLike[str]
PlatformName = Literal["linux", "macos", "windows", "pyodide"]
PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows", "pyodide"}
PlatformName = Literal["linux", "macos", "windows", "pyodide", "android", "ios"]
PLATFORMS: Final[frozenset[PlatformName]] = frozenset(typing.get_args(PlatformName))
class GenericPythonConfiguration(Protocol):
-985
View File
@@ -1,985 +0,0 @@
from __future__ import annotations
import contextlib
import enum
import fnmatch
import itertools
import os
import re
import shlex
import shutil
import ssl
import subprocess
import sys
import tarfile
import textwrap
import time
import typing
import urllib.request
from collections import defaultdict
from collections.abc import Generator, Iterable, Mapping, MutableMapping, Sequence
from dataclasses import dataclass
from enum import Enum
from functools import lru_cache, total_ordering
from pathlib import Path, PurePath
from tempfile import TemporaryDirectory
from time import sleep
from typing import Any, Final, Literal, TextIO, TypeVar
from zipfile import ZipFile
import bracex
import certifi
from filelock import FileLock
from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import SpecifierSet
from packaging.utils import parse_wheel_filename
from packaging.version import Version
from platformdirs import user_cache_path
from ._compat import tomllib
from .architecture import Architecture
from .errors import FatalError
from .typing import PathOrStr, PlatformName
__all__ = [
"MANYLINUX_ARCHS",
"EnableGroups",
"call",
"chdir",
"combine_constraints",
"find_compatible_wheel",
"find_uv",
"format_safe",
"get_build_verbosity_extra_flags",
"prepare_command",
"read_python_configs",
"resources_dir",
"selector_matches",
"shell",
"split_config_settings",
"strtobool",
]
resources_dir: Final[Path] = Path(__file__).parent / "resources"
install_certifi_script: Final[Path] = resources_dir / "install_certifi.py"
free_thread_enable_313: Final[Path] = resources_dir / "free-threaded-enable-313.xml"
test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py"
class EnableGroups(enum.Enum):
"""
Groups of build selectors that are not enabled by default.
"""
CPythonFreeThreading = "cpython-freethreading"
CPythonPrerelease = "cpython-prerelease"
PyPy = "pypy"
MANYLINUX_ARCHS: Final[tuple[str, ...]] = (
"x86_64",
"i686",
"pypy_x86_64",
"aarch64",
"ppc64le",
"s390x",
"armv7l",
"pypy_aarch64",
"pypy_i686",
)
MUSLLINUX_ARCHS: Final[tuple[str, ...]] = (
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
"armv7l",
)
DEFAULT_CIBW_CACHE_PATH: Final[Path] = user_cache_path(appname="cibuildwheel", appauthor="pypa")
CIBW_CACHE_PATH: Final[Path] = Path(
os.environ.get("CIBW_CACHE_PATH", DEFAULT_CIBW_CACHE_PATH)
).resolve()
IS_WIN: Final[bool] = sys.platform.startswith("win")
@typing.overload
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[False] = ...,
) -> None: ...
@typing.overload
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[True],
) -> str: ...
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: bool = False,
) -> str | None:
"""
Run subprocess.run, but print the commands first. Takes the commands as
*args. Uses shell=True on Windows due to a bug. Also converts to
Paths to strings, due to Windows behavior at least on older Pythons.
https://bugs.python.org/issue8557
"""
args_ = [str(arg) for arg in args]
# print the command executing for the logs
print("+ " + " ".join(shlex.quote(a) for a in args_))
# workaround platform behaviour differences outlined
# in https://github.com/python/cpython/issues/52803
path_env = env if env is not None else os.environ
path = path_env.get("PATH", None)
executable = shutil.which(args_[0], path=path)
if executable is None:
msg = f"Couldn't find {args_[0]!r} in PATH {path!r}"
raise FatalError(msg)
args_[0] = executable
try:
result = subprocess.run(
args_,
check=True,
shell=IS_WIN,
env=env,
cwd=cwd,
capture_output=capture_stdout,
text=capture_stdout,
)
except subprocess.CalledProcessError as e:
if capture_stdout:
sys.stderr.write(e.stderr)
raise
if not capture_stdout:
return None
sys.stderr.write(result.stderr)
return typing.cast(str, result.stdout)
def shell(
*commands: str, env: Mapping[str, str] | None = None, cwd: PathOrStr | None = None
) -> None:
command = " ".join(commands)
print(f"+ {command}")
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
def format_safe(template: str, **kwargs: str | os.PathLike[str]) -> str:
"""
Works similarly to `template.format(**kwargs)`, except that unmatched
fields in `template` are passed through untouched.
>>> format_safe('{a} {b}', a='123')
'123 {b}'
>>> format_safe('{a} {b[4]:3f}', a='123')
'123 {b[4]:3f}'
To avoid variable expansion, precede with a single backslash e.g.
>>> format_safe('\\{a} {b}', a='123')
'{a} {b}'
"""
result = template
for key, value in kwargs.items():
find_pattern = re.compile(
rf"""
(?<!\#) # don't match if preceded by a hash
{{ # literal open curly bracket
{re.escape(key)} # the field name
}} # literal close curly bracket
""",
re.VERBOSE,
)
result = re.sub(
pattern=find_pattern,
repl=str(value).replace("\\", r"\\"),
string=result,
)
# transform escaped sequences into their literal equivalents
result = result.replace(f"#{{{key}}}", f"{{{key}}}")
return result
def prepare_command(command: str, **kwargs: PathOrStr) -> str:
"""
Preprocesses a command by expanding variables like {python}.
For example, used in the test_command option to specify the path to the
project's root. Unmatched syntax will mostly be allowed through.
"""
return format_safe(command, python="python", pip="pip", **kwargs)
def get_build_verbosity_extra_flags(level: int) -> list[str]:
if level > 0:
return ["-" + level * "v"]
elif level < 0:
return ["-" + -level * "q"]
else:
return []
def split_config_settings(
config_settings: str, frontend: Literal["pip", "build", "build[uv]"]
) -> list[str]:
config_settings_list = shlex.split(config_settings)
s = "s" if frontend == "pip" else ""
return [f"--config-setting{s}={setting}" for setting in config_settings_list]
def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
input_file = resources_dir / "build-platforms.toml"
with input_file.open("rb") as f:
loaded_file = tomllib.load(f)
results: list[dict[str, str]] = list(loaded_file[config]["python_configurations"])
return results
def selector_matches(patterns: str, string: str) -> bool:
"""
Returns True if `string` is matched by any of the wildcard patterns in
`patterns`.
Matching is according to fnmatch, but with shell-like curly brace
expansion. For example, 'cp{36,37}-*' would match either of 'cp36-*' or
'cp37-*'.
"""
patterns_list = patterns.split()
expanded_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in patterns_list)
return any(fnmatch.fnmatch(string, pat) for pat in expanded_patterns)
# Once we require Python 3.10+, we can add kw_only=True
@dataclass(frozen=True)
class BuildSelector:
"""
This class holds a set of build/skip patterns. You call an instance with a
build identifier, and it returns True if that identifier should be
included. Only call this on valid identifiers, ones that have at least 2
numeric digits before the first dash.
"""
build_config: str
skip_config: str
requires_python: SpecifierSet | None = None
enable: frozenset[EnableGroups] = frozenset()
def __call__(self, build_id: str) -> bool:
# Filter build selectors by python_requires if set
if self.requires_python is not None:
py_ver_str = build_id.split("-")[0]
if py_ver_str.endswith("t"):
py_ver_str = py_ver_str[:-1]
major = int(py_ver_str[2])
minor = int(py_ver_str[3:])
version = Version(f"{major}.{minor}.99")
if not self.requires_python.contains(version):
return False
# filter out groups that are not enabled
if EnableGroups.CPythonFreeThreading not in self.enable and selector_matches(
"cp3??t-*", build_id
):
return False
if EnableGroups.CPythonPrerelease not in self.enable and selector_matches(
"cp314*", build_id
):
return False
if EnableGroups.PyPy not in self.enable and selector_matches("pp*", build_id):
return False
should_build = selector_matches(self.build_config, build_id)
should_skip = selector_matches(self.skip_config, build_id)
return should_build and not should_skip
def options_summary(self) -> Any:
return {
"build_config": self.build_config,
"skip_config": self.skip_config,
"requires_python": str(self.requires_python),
"enable": sorted(group.value for group in self.enable),
}
@dataclass(frozen=True)
class TestSelector:
"""
A build selector that can only skip tests according to a skip pattern.
"""
skip_config: str
def __call__(self, build_id: str) -> bool:
should_skip = selector_matches(self.skip_config, build_id)
return not should_skip
def options_summary(self) -> Any:
return {"skip_config": self.skip_config}
# Taken from https://stackoverflow.com/a/107717
class Unbuffered:
def __init__(self, stream: TextIO) -> None:
self.stream = stream
def write(self, data: str) -> None:
self.stream.write(data)
self.stream.flush()
def writelines(self, data: Iterable[str]) -> None:
self.stream.writelines(data)
self.stream.flush()
def __getattr__(self, attr: str) -> Any:
return getattr(self.stream, attr)
def download(url: str, dest: Path) -> None:
print(f"+ Download {url} to {dest}")
dest_dir = dest.parent
if not dest_dir.exists():
dest_dir.mkdir(parents=True)
# we've had issues when relying on the host OS' CA certificates on Windows,
# so we use certifi (this sounds odd but requests also does this by default)
cafile = os.environ.get("SSL_CERT_FILE", certifi.where())
context = ssl.create_default_context(cafile=cafile)
repeat_num = 3
for i in range(repeat_num):
try:
with urllib.request.urlopen(url, context=context) as response:
dest.write_bytes(response.read())
return
except OSError:
if i == repeat_num - 1:
raise
sleep(3)
def extract_zip(zip_src: Path, dest: Path) -> None:
with ZipFile(zip_src) as zip_:
for zinfo in zip_.filelist:
zip_.extract(zinfo, dest)
# Set permissions to the same values as they were set in the archive
# We have to do this manually due to
# https://github.com/python/cpython/issues/59999
# But some files in the zipfile seem to have external_attr with 0
# permissions. In that case just use the default value???
permissions = (zinfo.external_attr >> 16) & 0o777
if permissions != 0:
dest.joinpath(zinfo.filename).chmod(permissions)
def extract_tar(tar_src: Path, dest: Path) -> None:
with tarfile.open(tar_src) as tar_:
tar_.extraction_filter = getattr(tarfile, "tar_filter", (lambda member, _: member))
tar_.extractall(dest)
def move_file(src_file: Path, dst_file: Path) -> Path:
"""Moves a file safely while avoiding potential semantic confusion:
1. `dst_file` must point to the target filename, not a directory
2. `dst_file` will be overwritten if it already exists
3. any missing parent directories will be created
Returns the fully resolved Path of the resulting file.
Raises:
NotADirectoryError: If any part of the intermediate path to `dst_file` is an existing file
IsADirectoryError: If `dst_file` points directly to an existing directory
"""
src_file = src_file.resolve(strict=True)
dst_file = dst_file.resolve()
if dst_file.is_dir():
msg = "dst_file must be a valid target filename, not an existing directory."
raise IsADirectoryError(msg)
dst_file.unlink(missing_ok=True)
dst_file.parent.mkdir(parents=True, exist_ok=True)
# using shutil.move() as Path.rename() is not guaranteed to work across filesystem boundaries
# explicit str() needed for Python 3.8
resulting_file = shutil.move(str(src_file), str(dst_file))
return Path(resulting_file).resolve(strict=True)
class DependencyConstraints:
def __init__(self, base_file_path: Path):
assert base_file_path.exists()
self.base_file_path = base_file_path.resolve()
@staticmethod
def with_defaults() -> DependencyConstraints:
return DependencyConstraints(base_file_path=resources_dir / "constraints.txt")
def get_for_python_version(
self, version: str, *, variant: Literal["python", "pyodide"] = "python"
) -> Path:
version_parts = version.split(".")
# try to find a version-specific dependency file e.g. if
# ./constraints.txt is the base, look for ./constraints-python36.txt
specific_stem = self.base_file_path.stem + f"-{variant}{version_parts[0]}{version_parts[1]}"
specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name)
if specific_file_path.exists():
return specific_file_path
else:
return self.base_file_path
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.base_file_path!r})"
def __eq__(self, o: object) -> bool:
if not isinstance(o, DependencyConstraints):
return False
return self.base_file_path == o.base_file_path
def options_summary(self) -> Any:
if self == DependencyConstraints.with_defaults():
return "pinned"
else:
return self.base_file_path.name
BuildFrontendName = Literal["pip", "build", "build[uv]"]
@dataclass(frozen=True)
class BuildFrontendConfig:
name: BuildFrontendName
args: Sequence[str] = ()
@staticmethod
def from_config_string(config_string: str) -> BuildFrontendConfig:
config_dict = parse_key_value_string(config_string, ["name"], ["args"])
name = " ".join(config_dict["name"])
if name not in {"pip", "build", "build[uv]"}:
msg = f"Unrecognised build frontend {name!r}, only 'pip', 'build', and 'build[uv]' are supported"
raise ValueError(msg)
name = typing.cast(BuildFrontendName, name)
args = config_dict.get("args") or []
return BuildFrontendConfig(name=name, args=args)
def options_summary(self) -> str | dict[str, str]:
if not self.args:
return self.name
else:
return {"name": self.name, "args": repr(self.args)}
def strtobool(val: str) -> bool:
return val.lower() in {"y", "yes", "t", "true", "on", "1"}
class CIProvider(Enum):
travis_ci = "travis"
appveyor = "appveyor"
circle_ci = "circle_ci"
azure_pipelines = "azure_pipelines"
github_actions = "github_actions"
gitlab = "gitlab"
cirrus_ci = "cirrus_ci"
other = "other"
def detect_ci_provider() -> CIProvider | None:
if "TRAVIS" in os.environ:
return CIProvider.travis_ci
elif "APPVEYOR" in os.environ:
return CIProvider.appveyor
elif "CIRCLECI" in os.environ:
return CIProvider.circle_ci
elif "AZURE_HTTP_USER_AGENT" in os.environ:
return CIProvider.azure_pipelines
elif "GITHUB_ACTIONS" in os.environ:
return CIProvider.github_actions
elif "GITLAB_CI" in os.environ:
return CIProvider.gitlab
elif "CIRRUS_CI" in os.environ:
return CIProvider.cirrus_ci
elif strtobool(os.environ.get("CI", "false")):
return CIProvider.other
else:
return None
def unwrap(text: str) -> str:
"""
Unwraps multi-line text to a single line
"""
# remove initial line indent
text = textwrap.dedent(text)
# remove leading/trailing whitespace
text = text.strip()
# remove consecutive whitespace
return re.sub(r"\s+", " ", text)
@dataclass(frozen=True)
class FileReport:
name: str
size: str
@contextlib.contextmanager
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
"""
Prints the new items in a directory upon exiting. The message to display
can include {n} for number of wheels, {s} for total number of seconds,
and/or {m} for total number of minutes. Does not print anything if this
exits via exception.
"""
start_time = time.time()
existing_contents = set(output_dir.iterdir())
yield
final_contents = set(output_dir.iterdir())
new_contents = [
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
for wheel in final_contents - existing_contents
]
if not new_contents:
return
max_name_len = max(len(f.name) for f in new_contents)
max_size_len = max(len(f.size) for f in new_contents)
n = len(new_contents)
s = time.time() - start_time
m = s / 60
print(
msg.format(n=n, s=s, m=m),
*sorted(
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
),
sep="\n",
)
def get_pip_version(env: Mapping[str, str]) -> str:
versions_output_text = call(
"python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
)
(pip_version,) = (
version[5:]
for version in versions_output_text.strip().splitlines()
if version.startswith("pip==")
)
return pip_version
@lru_cache(maxsize=None)
def ensure_node(major_version: str) -> Path:
input_file = resources_dir / "nodejs.toml"
with input_file.open("rb") as f:
loaded_file = tomllib.load(f)
version = str(loaded_file[major_version])
base_url = str(loaded_file["url"])
ext = "zip" if IS_WIN else "tar.xz"
platform = "win" if IS_WIN else ("darwin" if sys.platform.startswith("darwin") else "linux")
linux_arch = Architecture.native_arch("linux")
assert linux_arch is not None
arch = {"x86_64": "x64", "i686": "x86", "aarch64": "arm64"}.get(
linux_arch.value, linux_arch.value
)
name = f"node-{version}-{platform}-{arch}"
path = CIBW_CACHE_PATH / name
with FileLock(str(path) + ".lock"):
if not path.exists():
url = f"{base_url}{version}/{name}.{ext}"
with TemporaryDirectory() as tmp_path:
archive = Path(tmp_path) / f"{name}.{ext}"
download(url, archive)
if ext == "zip":
extract_zip(archive, path.parent)
else:
extract_tar(archive, path.parent)
assert path.exists()
if not IS_WIN:
return path / "bin"
return path
@lru_cache(maxsize=None)
def _ensure_virtualenv(version: str) -> Path:
version_parts = version.split(".")
key = f"py{version_parts[0]}{version_parts[1]}"
input_file = resources_dir / "virtualenv.toml"
with input_file.open("rb") as f:
loaded_file = tomllib.load(f)
configuration = loaded_file.get(key, loaded_file["default"])
version = str(configuration["version"])
url = str(configuration["url"])
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
with FileLock(str(path) + ".lock"):
if not path.exists():
download(url, path)
return path
def _parse_constraints_for_virtualenv(
seed_packages: list[str],
dependency_constraint_flags: Sequence[PathOrStr],
) -> dict[str, str]:
"""
Parses the constraints file referenced by `dependency_constraint_flags` and returns a dict where
the key is the package name, and the value is the constraint version.
If a package version cannot be found, its value is "embed" meaning that virtualenv will install
its bundled version, already available locally.
The function does not try to be too smart and just handles basic constraints.
If it can't get an exact version, the real constraint will be handled by the
{macos|windows}.setup_python function.
"""
assert len(dependency_constraint_flags) in {0, 2}
# only seed pip if other seed packages do not appear in a constraint file
constraints_dict = {"pip": "embed"}
if len(dependency_constraint_flags) == 2:
assert dependency_constraint_flags[0] == "-c"
constraint_path = Path(dependency_constraint_flags[1])
assert constraint_path.exists()
with constraint_path.open(encoding="utf-8") as constraint_file:
for line_ in constraint_file:
line = line_.strip()
if not line:
continue
if line.startswith("#"):
continue
try:
requirement = Requirement(line)
package = requirement.name
if (
package not in seed_packages
or requirement.url is not None
or requirement.marker is not None
or len(requirement.extras) != 0
or len(requirement.specifier) != 1
):
continue
specifier = next(iter(requirement.specifier))
if specifier.operator != "==":
continue
constraints_dict[package] = specifier.version
except InvalidRequirement:
continue
return constraints_dict
def virtualenv(
version: str,
python: Path,
venv_path: Path,
dependency_constraint_flags: Sequence[PathOrStr],
*,
use_uv: bool,
) -> dict[str, str]:
"""
Create a virtual environment. If `use_uv` is True,
dependency_constraint_flags are ignored since nothing is installed in the
venv. Otherwise, pip is installed, and setuptools + wheel if Python < 3.12.
"""
# virtualenv may fail if this is a symlink.
python = python.resolve()
assert python.exists()
if use_uv:
call("uv", "venv", venv_path, "--python", python)
else:
virtualenv_app = _ensure_virtualenv(version)
allowed_seed_packages = ["pip", "setuptools", "wheel"]
constraints = _parse_constraints_for_virtualenv(
allowed_seed_packages, dependency_constraint_flags
)
additional_flags: list[str] = []
for package in allowed_seed_packages:
if package in constraints:
additional_flags.append(f"--{package}={constraints[package]}")
else:
additional_flags.append(f"--no-{package}")
# Using symlinks to pre-installed seed packages is really the fastest way to get a virtual
# environment. The initial cost is a bit higher but reusing is much faster.
# Windows does not always allow symlinks so just disabling for now.
# Requires pip>=19.3 so disabling for "embed" because this means we don't know what's the
# version of pip that will end-up installed.
# c.f. https://virtualenv.pypa.io/en/latest/cli_interface.html#section-seeder
if (
not IS_WIN
and constraints["pip"] != "embed"
and Version(constraints["pip"]) >= Version("19.3")
):
additional_flags.append("--symlink-app-data")
call(
sys.executable,
"-sS", # just the stdlib, https://github.com/pypa/virtualenv/issues/2133#issuecomment-1003710125
virtualenv_app,
"--activators=",
"--no-periodic-update",
*additional_flags,
"--python",
python,
venv_path,
)
paths = [str(venv_path), str(venv_path / "Scripts")] if IS_WIN else [str(venv_path / "bin")]
env = os.environ.copy()
env["PATH"] = os.pathsep.join([*paths, env["PATH"]])
env["VIRTUAL_ENV"] = str(venv_path)
return env
T = TypeVar("T", bound=PurePath)
def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None:
"""
Finds a wheel with an abi3 or a none ABI tag in `wheels` compatible with the Python interpreter
specified by `identifier` that is previously built.
"""
interpreter, platform = identifier.split("-")
free_threaded = interpreter.endswith("t")
if free_threaded:
interpreter = interpreter[:-1]
for wheel in wheels:
_, _, _, tags = parse_wheel_filename(wheel.name)
for tag in tags:
if tag.abi == "abi3" and not free_threaded:
# ABI3 wheels must start with cp3 for impl and tag
if not (interpreter.startswith("cp3") and tag.interpreter.startswith("cp3")):
continue
elif tag.abi == "none":
# CPythonless wheels must include py3 tag
if tag.interpreter[:3] != "py3":
continue
else:
# Other types of wheels are not detected, this is looking for previously built wheels.
continue
if tag.interpreter != "py3" and int(tag.interpreter[3:]) > int(interpreter[3:]):
# If a minor version number is given, it has to be lower than the current one.
continue
if platform.startswith(("manylinux", "musllinux", "macosx")):
# Linux, macOS require the beginning and ending match (macos/manylinux version doesn't need to)
os_, arch = platform.split("_", 1)
if not tag.platform.startswith(os_):
continue
if not tag.platform.endswith(f"_{arch}"):
continue
else:
# Windows should exactly match
if tag.platform != platform:
continue
# If all the filters above pass, then the wheel is a previously built compatible wheel.
return wheel
return None
# Can be replaced by contextlib.chdir in Python 3.11
@contextlib.contextmanager
def chdir(new_path: Path | str) -> Generator[None, None, None]:
"""Non thread-safe context manager to change the current working directory."""
cwd = os.getcwd()
try:
os.chdir(new_path)
yield
finally:
os.chdir(cwd)
def fix_ansi_codes_for_github_actions(text: str) -> str:
"""
Github Actions forgets the current ANSI style on every new line. This
function repeats the current ANSI style on every new line.
"""
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
ansi_codes: list[str] = []
output = ""
for line in text.splitlines(keepends=True):
# add the current ANSI codes to the beginning of the line
output += "".join(ansi_codes) + line
# split the line at each ANSI code
parts = ansi_code_regex.split(line)
# if there are any ANSI codes, save them
if len(parts) > 1:
# iterate over the ANSI codes in this line
for code in parts[1::2]:
if code == "\033[0m":
# reset the list of ANSI codes when the clear code is found
ansi_codes = []
else:
ansi_codes.append(code)
return output
def parse_key_value_string(
key_value_string: str,
positional_arg_names: Sequence[str] | None = None,
kw_arg_names: Sequence[str] | None = None,
) -> dict[str, list[str]]:
"""
Parses a string like "docker; create_args: --some-option=value another-option"
"""
if positional_arg_names is None:
positional_arg_names = []
if kw_arg_names is None:
kw_arg_names = []
all_field_names = [*positional_arg_names, *kw_arg_names]
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
shlexer.commenters = ""
shlexer.whitespace_split = True
parts = list(shlexer)
# parts now looks like
# ['docker', ';', 'create_args:', '--some-option=value', 'another-option']
# split by semicolon
fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k]
result: defaultdict[str, list[str]] = defaultdict(list)
for field_i, field in enumerate(fields):
# check to see if the option name is specified
field_name, sep, first_value = field[0].partition(":")
if sep:
if field_name not in all_field_names:
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
raise ValueError(msg)
values = ([first_value] if first_value else []) + field[1:]
else:
try:
field_name = positional_arg_names[field_i]
except IndexError:
msg = f"Failed to parse {key_value_string!r}. Too many positional arguments - expected a maximum of {len(positional_arg_names)}"
raise ValueError(msg) from None
values = field
result[field_name] += values
return dict(result)
def find_uv() -> Path | None:
# Prefer uv in our environment
with contextlib.suppress(ImportError, FileNotFoundError):
# pylint: disable-next=import-outside-toplevel
from uv import find_uv_bin
return Path(find_uv_bin())
uv_on_path = shutil.which("uv")
return Path(uv_on_path) if uv_on_path else None
def combine_constraints(
env: MutableMapping[str, str], /, constraints_path: Path, tmp_dir: Path | None
) -> None:
"""
This will workaround a bug in pip<=21.1.1 or uv<=0.2.0 if a tmp_dir is given.
If set to None, this will use the modern URI method.
"""
if tmp_dir:
if " " in str(constraints_path):
assert " " not in str(tmp_dir)
tmp_file = tmp_dir / "constraints.txt"
tmp_file.write_bytes(constraints_path.read_bytes())
constraints_path = tmp_file
our_constraints = str(constraints_path)
else:
our_constraints = (
constraints_path.as_uri() if " " in str(constraints_path) else str(constraints_path)
)
user_constraints = env.get("PIP_CONSTRAINT")
env["UV_CONSTRAINT"] = env["PIP_CONSTRAINT"] = " ".join(
c for c in [our_constraints, user_constraints] if c
)
@total_ordering
class FlexibleVersion:
version_str: str
version_parts: tuple[int, ...]
suffix: str
def __init__(self, version_str: str) -> None:
self.version_str = version_str
# Split into numeric parts and the optional suffix
match = re.match(r"^[v]?(\d+(\.\d+)*)(.*)$", version_str)
if not match:
msg = f"Invalid version string: {version_str}"
raise ValueError(msg)
version_part, _, suffix = match.groups()
# Convert numeric version part into a tuple of integers
self.version_parts = tuple(map(int, version_part.split(".")))
self.suffix = suffix.strip() if suffix else ""
# Normalize by removing trailing zeros
self.version_parts = self._remove_trailing_zeros(self.version_parts)
def _remove_trailing_zeros(self, parts: tuple[int, ...]) -> tuple[int, ...]:
# Remove trailing zeros for accurate comparisons
# without this, "3.0" would be considered greater than "3"
while parts and parts[-1] == 0:
parts = parts[:-1]
return parts
def __eq__(self, other: object) -> bool:
if not isinstance(other, FlexibleVersion):
raise NotImplementedError()
return (self.version_parts, self.suffix) == (other.version_parts, other.suffix)
def __lt__(self, other: object) -> bool:
if not isinstance(other, FlexibleVersion):
raise NotImplementedError()
return (self.version_parts, self.suffix) < (other.version_parts, other.suffix)
def __repr__(self) -> str:
return f"FlexibleVersion('{self.version_str}')"
def __str__(self) -> str:
return self.version_str
View File
+98
View File
@@ -0,0 +1,98 @@
import os
import shlex
import shutil
import subprocess
import sys
import typing
from collections.abc import Iterator, Mapping
from typing import Final, Literal
from ..errors import FatalError
from ..typing import PathOrStr
_IS_WIN: Final[bool] = sys.platform.startswith("win")
@typing.overload
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[False] = ...,
) -> None: ...
@typing.overload
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[True],
) -> str: ...
def call(
*args: PathOrStr,
env: Mapping[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: bool = False,
) -> str | None:
"""
Run subprocess.run, but print the commands first. Takes the commands as
*args. Uses shell=True on Windows due to a bug. Also converts to
Paths to strings, due to Windows behavior at least on older Pythons.
https://bugs.python.org/issue8557
"""
args_ = [str(arg) for arg in args]
# print the command executing for the logs
print("+ " + " ".join(shlex.quote(a) for a in args_))
# workaround platform behaviour differences outlined
# in https://github.com/python/cpython/issues/52803
path_env = env if env is not None else os.environ
path = path_env.get("PATH", None)
executable = shutil.which(args_[0], path=path)
if executable is None:
msg = f"Couldn't find {args_[0]!r} in PATH {path!r}"
raise FatalError(msg)
args_[0] = executable
try:
result = subprocess.run(
args_,
check=True,
shell=_IS_WIN,
env=env,
cwd=cwd,
capture_output=capture_stdout,
text=capture_stdout,
)
except subprocess.CalledProcessError as e:
if capture_stdout:
sys.stderr.write(e.stderr)
raise
if not capture_stdout:
return None
sys.stderr.write(result.stderr)
return typing.cast(str, result.stdout)
def shell(
*commands: str, env: Mapping[str, str] | None = None, cwd: PathOrStr | None = None
) -> None:
command = " ".join(commands)
print(f"+ {command}")
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
def split_command(lst: list[str]) -> Iterator[list[str]]:
"""
Split a shell-style command, as returned by shlex.split, into a sequence
of commands, separated by '&&'.
"""
items = list[str]()
for item in lst:
if item == "&&":
yield items
items = []
else:
items.append(item)
yield items
+134
View File
@@ -0,0 +1,134 @@
import os
import shutil
import ssl
import tarfile
import time
import urllib.request
from collections.abc import Callable
from pathlib import Path, PurePath
from typing import Final
from zipfile import ZipFile
import certifi
from platformdirs import user_cache_path
from ..errors import FatalError
DEFAULT_CIBW_CACHE_PATH: Final[Path] = user_cache_path(appname="cibuildwheel", appauthor="pypa")
CIBW_CACHE_PATH: Final[Path] = Path(
os.environ.get("CIBW_CACHE_PATH", DEFAULT_CIBW_CACHE_PATH)
).resolve()
def download(url: str, dest: Path) -> None:
print(f"+ Download {url} to {dest}")
dest_dir = dest.parent
dest_dir.mkdir(parents=True, exist_ok=True)
# we've had issues when relying on the host OS' CA certificates on Windows,
# so we use certifi (this sounds odd but requests also does this by default)
cafile = os.environ.get("SSL_CERT_FILE", certifi.where())
context = ssl.create_default_context(cafile=cafile)
repeat_num = 3
for i in range(repeat_num):
try:
with urllib.request.urlopen(url, context=context) as response:
dest.write_bytes(response.read())
return
except OSError:
if i == repeat_num - 1:
raise
time.sleep(3)
def extract_zip(zip_src: Path, dest: Path) -> None:
"""Extracts a zip and correctly sets permissions on extracted files.
Notes:
- sets permissions to the same values as they were set in the archive
- files with no clear permissions in `external_attr` will be extracted with default values
"""
with ZipFile(zip_src) as zip_:
for zinfo in zip_.filelist:
zip_.extract(zinfo, dest)
# Set permissions to the same values as they were set in the archive
# We have to do this manually due to https://github.com/python/cpython/issues/59999
permissions = (zinfo.external_attr >> 16) & 0o777
if permissions != 0:
dest.joinpath(zinfo.filename).chmod(permissions)
def extract_tar(tar_src: Path, dest: Path) -> None:
"""Extracts a tar file using the stdlib 'tar' filter.
See: https://docs.python.org/3/library/tarfile.html#tarfile.tar_filter for filter details
"""
with tarfile.open(tar_src) as tar_:
tar_.extraction_filter = getattr(tarfile, "tar_filter", (lambda member, _: member))
tar_.extractall(dest)
def move_file(src_file: Path, dst_file: Path) -> Path:
"""Moves a file safely while avoiding potential semantic confusion:
1. `dst_file` must point to the target filename, not a directory
2. `dst_file` will be overwritten if it already exists
3. any missing parent directories will be created
Returns the fully resolved Path of the resulting file.
Raises:
NotADirectoryError: If any part of the intermediate path to `dst_file` is an existing file
IsADirectoryError: If `dst_file` points directly to an existing directory
"""
src_file = src_file.resolve(strict=True)
dst_file = dst_file.resolve()
if dst_file.is_dir():
msg = "dst_file must be a valid target filename, not an existing directory."
raise IsADirectoryError(msg)
dst_file.unlink(missing_ok=True)
dst_file.parent.mkdir(parents=True, exist_ok=True)
# using shutil.move() as Path.rename() is not guaranteed to work across filesystem boundaries
# explicit str() needed for Python 3.8
resulting_file = shutil.move(str(src_file), str(dst_file))
return Path(resulting_file).resolve(strict=True)
def copy_into_local(src: Path, dst: PurePath) -> None:
"""Copy a path from src to dst, regardless of whether it's a file or a directory."""
# Ensure the target folder location exists
Path(dst.parent).mkdir(exist_ok=True, parents=True)
if src.is_dir():
shutil.copytree(src, dst)
else:
shutil.copy(src, dst)
def copy_test_sources(
test_sources: list[str],
project_dir: Path,
test_dir: PurePath,
copy_into: Callable[[Path, PurePath], None] = copy_into_local,
) -> None:
"""Copy the list of test sources from the package to the test directory.
:param test_sources: A list of test paths, relative to the project_dir.
:param project_dir: The root of the project.
:param test_dir: The folder where test sources should be placed.
:param copy_into: The copy function to use. By default, does a local
filesystem copy; but an OCIContainer.copy_info method (or equivalent)
can be provided.
"""
for test_path in test_sources:
source = project_dir.resolve() / test_path
if not source.exists():
msg = f"Test source {test_path} does not exist."
raise FatalError(msg)
copy_into(source, test_dir / test_path)
+174
View File
@@ -0,0 +1,174 @@
import dataclasses
import itertools
import os
import re
import shlex
import textwrap
from collections import defaultdict
from collections.abc import Sequence
from ..typing import PathOrStr
def format_safe(template: str, **kwargs: str | os.PathLike[str]) -> str:
"""
Works similarly to `template.format(**kwargs)`, except that unmatched
fields in `template` are passed through untouched.
>>> format_safe('{a} {b}', a='123')
'123 {b}'
>>> format_safe('{a} {b[4]:3f}', a='123')
'123 {b[4]:3f}'
To avoid variable expansion, precede with a single backslash e.g.
>>> format_safe('\\{a} {b}', a='123')
'{a} {b}'
"""
result = template
for key, value in kwargs.items():
find_pattern = re.compile(
rf"""
(?<!\#) # don't match if preceded by a hash
{{ # literal open curly bracket
{re.escape(key)} # the field name
}} # literal close curly bracket
""",
re.VERBOSE,
)
result = re.sub(
pattern=find_pattern,
repl=str(value).replace("\\", r"\\"),
string=result,
)
# transform escaped sequences into their literal equivalents
result = result.replace(f"#{{{key}}}", f"{{{key}}}")
return result
def prepare_command(command: str, **kwargs: PathOrStr) -> str:
"""
Preprocesses a command by expanding variables like {project}.
For example, used in the test_command option to specify the path to the
project's root. Unmatched syntax will mostly be allowed through.
"""
return format_safe(command, **kwargs)
def strtobool(val: str) -> bool:
return val.lower() in {"y", "yes", "t", "true", "on", "1"}
def unwrap(text: str) -> str:
"""
Unwraps multi-line text to a single line
"""
# remove initial line indent
text = textwrap.dedent(text)
# remove leading/trailing whitespace
text = text.strip()
# remove consecutive whitespace
return re.sub(r"\s+", " ", text)
def unwrap_preserving_paragraphs(text: str) -> str:
"""
Unwraps multi-line text to a single line, but preserves paragraphs
"""
# remove initial line indent
text = textwrap.dedent(text)
# remove leading/trailing whitespace
text = text.strip()
paragraphs = text.split("\n\n")
# remove consecutive whitespace
paragraphs = [re.sub(r"\s+", " ", paragraph) for paragraph in paragraphs]
return "\n\n".join(paragraphs)
def parse_key_value_string(
key_value_string: str,
positional_arg_names: Sequence[str] | None = None,
kw_arg_names: Sequence[str] | None = None,
) -> dict[str, list[str]]:
"""
Parses a string like "docker; create_args: --some-option=value another-option"
"""
if positional_arg_names is None:
positional_arg_names = []
if kw_arg_names is None:
kw_arg_names = []
all_field_names = [*positional_arg_names, *kw_arg_names]
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
shlexer.commenters = ""
shlexer.whitespace_split = True
parts = list(shlexer)
# parts now looks like
# ['docker', ';', 'create_args:', '--some-option=value', 'another-option']
# split by semicolon
fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k]
result: defaultdict[str, list[str]] = defaultdict(list)
for field_i, field in enumerate(fields):
# check to see if the option name is specified
field_name, sep, first_value = field[0].partition(":")
if sep:
if field_name not in all_field_names:
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
raise ValueError(msg)
values = ([first_value] if first_value else []) + field[1:]
else:
try:
field_name = positional_arg_names[field_i]
except IndexError:
msg = f"Failed to parse {key_value_string!r}. Too many positional arguments - expected a maximum of {len(positional_arg_names)}"
raise ValueError(msg) from None
values = field
result[field_name] += values
return dict(result)
@dataclasses.dataclass(order=True)
class FlexibleVersion:
version_parts: tuple[int, ...] = dataclasses.field(init=False, repr=False)
suffix: str = dataclasses.field(init=False, repr=False)
version_str: str = dataclasses.field(compare=False)
def __post_init__(self) -> None:
# Split into numeric parts and the optional suffix
match = re.match(r"^[v]?(\d+(\.\d+)*)(.*)$", self.version_str)
if not match:
msg = f"Invalid version string: {self.version_str}"
raise ValueError(msg)
version_part, _, suffix = match.groups()
# Convert numeric version part into a tuple of integers
self.version_parts = tuple(map(int, version_part.split(".")))
self.suffix = suffix.strip() if suffix else ""
# Normalize by removing trailing zeros
self.version_parts = self._remove_trailing_zeros(self.version_parts)
@staticmethod
def _remove_trailing_zeros(parts: tuple[int, ...]) -> tuple[int, ...]:
# Remove trailing zeros for accurate comparisons
# without this, "3.0" would be considered greater than "3"
while parts and parts[-1] == 0:
parts = parts[:-1]
return parts
def __str__(self) -> str:
return self.version_str
+207
View File
@@ -0,0 +1,207 @@
import shlex
from collections.abc import Mapping, MutableMapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path, PurePath
from typing import Any, Literal, Self, TypeVar
from packaging.utils import parse_wheel_filename
from . import resources
from .cmd import call
from .helpers import parse_key_value_string, unwrap
@dataclass(kw_only=True)
class DependencyConstraints:
base_file_path: Path | None = None
packages: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
if self.packages and self.base_file_path is not None:
msg = "Cannot specify both a file and packages in the dependency constraints"
raise ValueError(msg)
if self.base_file_path is not None:
if not self.base_file_path.exists():
msg = f"Dependency constraints file not found: {self.base_file_path}"
raise FileNotFoundError(msg)
self.base_file_path = self.base_file_path.resolve()
@classmethod
def pinned(cls) -> Self:
return cls(base_file_path=resources.CONSTRAINTS)
@classmethod
def latest(cls) -> Self:
return cls()
@classmethod
def from_config_string(cls, config_string: str) -> Self:
if config_string == "pinned":
return cls.pinned()
if config_string == "latest" or not config_string:
return cls.latest()
if config_string.startswith(("file:", "packages:")):
# we only do the table-style parsing if it looks like a table,
# because this option used to be only a file path. We don't want
# to break existing configurations, whose file paths might include
# special characters like ':' or ' ', which would require quoting
# if they were to be passed as a parse_key_value_string positional
# argument.
return cls.from_table_style_config_string(config_string)
return cls(base_file_path=Path(config_string))
@classmethod
def from_table_style_config_string(cls, config_string: str) -> Self:
config_dict = parse_key_value_string(config_string, kw_arg_names=["file", "packages"])
files = config_dict.get("file")
packages = config_dict.get("packages") or []
if files and packages:
msg = "Cannot specify both a file and packages in dependency-versions"
raise ValueError(msg)
if files:
if len(files) > 1:
msg = unwrap("""
Only one file can be specified in dependency-versions.
If you intended to pass only one, perhaps you need to quote the path?
""")
raise ValueError(msg)
return cls(base_file_path=Path(files[0]))
return cls(packages=packages)
def get_for_python_version(
self, *, version: str, variant: Literal["python", "pyodide"] = "python", tmp_dir: Path
) -> Path | None:
if self.packages:
constraint_file = tmp_dir / "constraints.txt"
constraint_file.write_text("\n".join(self.packages))
return constraint_file
if self.base_file_path is not None:
version_parts = version.split(".")
# try to find a version-specific dependency file e.g. if
# ./constraints.txt is the base, look for ./constraints-python36.txt
specific_stem = (
self.base_file_path.stem + f"-{variant}{version_parts[0]}{version_parts[1]}"
)
specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name)
if specific_file_path.exists():
return specific_file_path
else:
return self.base_file_path
return None
def options_summary(self) -> Any:
if self == DependencyConstraints.pinned():
return "pinned"
elif self.packages:
return {"packages": " ".join(shlex.quote(p) for p in self.packages)}
elif self.base_file_path is not None:
return self.base_file_path.name
else:
return "latest"
def get_pip_version(env: Mapping[str, str]) -> str:
versions_output_text = call(
"python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
)
(pip_version,) = (
version[5:]
for version in versions_output_text.strip().splitlines()
if version.startswith("pip==")
)
return pip_version
T = TypeVar("T", bound=PurePath)
def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None:
"""
Finds a wheel with an abi3 or a none ABI tag in `wheels` compatible with the Python interpreter
specified by `identifier` that is previously built.
"""
interpreter, platform = identifier.split("-", 1)
interpreter = interpreter.split("_")[0]
free_threaded = interpreter.endswith("t")
if free_threaded:
interpreter = interpreter[:-1]
for wheel in wheels:
_, _, _, tags = parse_wheel_filename(wheel.name)
for tag in tags:
if tag.abi == "abi3" and not free_threaded:
# ABI3 wheels must start with cp3 for impl and tag
if not (interpreter.startswith("cp3") and tag.interpreter.startswith("cp3")):
continue
elif tag.abi == "none":
# CPythonless wheels must include py3 tag
if tag.interpreter[:3] != "py3":
continue
else:
# Other types of wheels are not detected, this is looking for previously built wheels.
continue
if tag.interpreter != "py3" and int(tag.interpreter[3:]) > int(interpreter[3:]):
# If a minor version number is given, it has to be lower than the current one.
continue
if platform.startswith(("manylinux", "musllinux", "macosx", "android", "ios")):
# On these platforms the wheel tag includes a platform version number, which we
# should ignore.
os_, arch = platform.split("_", 1)
if not tag.platform.startswith(os_):
continue
if not tag.platform.endswith(f"_{arch}"):
continue
elif platform.startswith("pyodide"):
# each Pyodide version has its own platform tag
continue
else:
# Windows should exactly match
if tag.platform != platform:
continue
# If all the filters above pass, then the wheel is a previously built compatible wheel.
return wheel
return None
def combine_constraints(
env: MutableMapping[str, str], /, constraints_path: Path, tmp_dir: Path | None
) -> None:
"""
This will workaround a bug in pip<=21.1.1 or uv<=0.2.0 if a tmp_dir is given.
If set to None, this will use the modern URI method.
"""
if tmp_dir:
if " " in str(constraints_path):
assert " " not in str(tmp_dir)
tmp_file = tmp_dir / "constraints.txt"
tmp_file.write_bytes(constraints_path.read_bytes())
constraints_path = tmp_file
our_constraints = str(constraints_path)
else:
our_constraints = (
constraints_path.as_uri() if " " in str(constraints_path) else str(constraints_path)
)
user_constraints = env.get("PIP_CONSTRAINT")
env["UV_CONSTRAINT"] = env["PIP_CONSTRAINT"] = " ".join(
c for c in [our_constraints, user_constraints] if c
)
@@ -0,0 +1,181 @@
import fnmatch
import functools
import json
import platform
import typing
from pathlib import Path
from filelock import FileLock
from cibuildwheel.util.file import download, extract_tar
from cibuildwheel.util.resources import PYTHON_BUILD_STANDALONE_RELEASES
class PythonBuildStandaloneAsset(typing.TypedDict):
name: str
url: str
class PythonBuildStandaloneRelease(typing.TypedDict):
tag: str
assets: list[PythonBuildStandaloneAsset]
class PythonBuildStandaloneReleaseData(typing.TypedDict):
releases: list[PythonBuildStandaloneRelease]
@functools.cache
def get_python_build_standalone_release_data() -> PythonBuildStandaloneReleaseData:
with open(PYTHON_BUILD_STANDALONE_RELEASES, "rb") as f:
return typing.cast(PythonBuildStandaloneReleaseData, json.load(f))
class PythonBuildStandaloneError(Exception):
"""Errors related to python-build-standalone."""
def _get_platform_identifiers() -> tuple[str, str, str | None]:
"""
Detects the current platform and returns architecture, platform, and libc
identifiers.
"""
system = platform.system()
machine = platform.machine()
machine_lower = machine.lower()
arch_identifier: str
platform_identifier: str
libc_identifier: str | None = None
# Map Architecture
if machine_lower in ["x86_64", "amd64"]:
arch_identifier = "x86_64"
elif machine_lower in ["aarch64", "arm64"]:
arch_identifier = "aarch64"
else:
msg = f"Unsupported architecture: {system} {machine}. Cannot download appropriate Python build."
raise PythonBuildStandaloneError(msg)
# Map OS + Libc
if system == "Linux":
platform_identifier = "unknown-linux"
libc_identifier = "musl" if "musl" in (platform.libc_ver() or ("", "")) else "gnu"
elif system == "Darwin":
platform_identifier = "apple-darwin"
elif system == "Windows":
platform_identifier = "pc-windows-msvc"
else:
msg = f"Unsupported operating system: {system}. Cannot download appropriate Python build."
raise PythonBuildStandaloneError(msg)
print(
f"Detected platform: arch='{arch_identifier}', platform='{platform_identifier}', libc='{libc_identifier}'"
)
return arch_identifier, platform_identifier, libc_identifier
def _get_pbs_asset(
*,
python_version: str,
arch_identifier: str,
platform_identifier: str,
libc_identifier: str | None,
) -> tuple[str, str, str]:
"""Finds the asset, returning (tag, filename, url)."""
release_data = get_python_build_standalone_release_data()
expected_suffix = f"{arch_identifier}-{platform_identifier}"
if libc_identifier:
expected_suffix += f"-{libc_identifier}"
expected_suffix += "-install_only.tar.gz"
asset_pattern = f"cpython-{python_version}.*-{expected_suffix}"
print(f"Looking for file with pattern {asset_pattern}")
for release in release_data["releases"]:
for asset in release["assets"]:
asset_name = asset["name"]
if not fnmatch.fnmatch(asset_name, asset_pattern):
continue
asset_url = asset["url"]
return release["tag"], asset_url, asset_name
# If loop completes without finding a match
msg = f"Could not find python-build-standalone release asset matching {asset_pattern!r}."
raise PythonBuildStandaloneError(msg)
def _download_or_get_from_cache(asset_url: str, asset_filename: str, cache_dir: Path) -> Path:
with FileLock(cache_dir / (asset_filename + ".lock")):
asset_cache_path = cache_dir / asset_filename
if asset_cache_path.is_file():
print(f"Using cached python_build_standalone: {asset_cache_path}")
return asset_cache_path
print(f"Downloading python_build_standalone: {asset_url} to {asset_cache_path}")
download(asset_url, asset_cache_path)
return asset_cache_path
def _find_python_executable(extracted_dir: Path) -> Path:
"""Finds the python executable within the extracted directory structure."""
# Structure is typically 'python/bin/python' or 'python/python.exe'
base_install_dir = extracted_dir / "python"
if platform.system() == "Windows":
executable_path = base_install_dir / "python.exe"
else:
executable_path = base_install_dir / "bin" / "python"
if not executable_path.is_file():
msg = f"Could not locate python executable at expected path {executable_path} within {extracted_dir}."
raise PythonBuildStandaloneError(msg)
print(f"Found python executable: {executable_path}")
return executable_path.resolve() # Return absolute path
def create_python_build_standalone_environment(
python_version: str, temp_dir: Path, cache_dir: Path
) -> Path:
"""
Returns a Python environment from python-build-standalone, downloading it
if necessary using a cache, and expanding it into a fresh base path.
Args:
python_version: The Python version string (e.g., "3.12").
temp_dir: A directory where the Python environment will be created.
cache_dir: A directory to store/retrieve downloaded archives.
Returns:
The absolute path to the python executable within the created environment (in temp_dir).
Raises:
PythonBuildStandaloneError: If the platform is unsupported, the build cannot be found,
download/extraction fails, or configuration is invalid.
"""
print(f"Creating python-build-standalone environment: version={python_version}")
arch_id, platform_id, libc_id = _get_platform_identifiers()
pbs_tag, asset_url, asset_filename = _get_pbs_asset(
python_version=python_version,
arch_identifier=arch_id,
platform_identifier=platform_id,
libc_identifier=libc_id,
)
print(f"Using python-build-standalone release: {pbs_tag}")
archive_path = _download_or_get_from_cache(
asset_url=asset_url, asset_filename=asset_filename, cache_dir=cache_dir
)
python_base_dir = temp_dir / "pbs"
assert not python_base_dir.exists()
extract_tar(archive_path, python_base_dir)
return _find_python_executable(python_base_dir)
+32
View File
@@ -0,0 +1,32 @@
import functools
import tomllib
from pathlib import Path
from typing import Final
from ..typing import PlatformName
PATH: Final[Path] = Path(__file__).parent.parent / "resources"
INSTALL_CERTIFI_SCRIPT: Final[Path] = PATH / "install_certifi.py"
FREE_THREAD_ENABLE_313: Final[Path] = PATH / "free-threaded-enable-313.xml"
FREE_THREAD_ENABLE_314: Final[Path] = PATH / "free-threaded-enable-314.xml"
NODEJS: Final[Path] = PATH / "nodejs.toml"
DEFAULTS: Final[Path] = PATH / "defaults.toml"
PINNED_DOCKER_IMAGES: Final[Path] = PATH / "pinned_docker_images.cfg"
BUILD_PLATFORMS: Final[Path] = PATH / "build-platforms.toml"
CONSTRAINTS: Final[Path] = PATH / "constraints.txt"
VIRTUALENV: Final[Path] = PATH / "virtualenv.toml"
CIBUILDWHEEL_SCHEMA: Final[Path] = PATH / "cibuildwheel.schema.json"
PYTHON_BUILD_STANDALONE_RELEASES: Final[Path] = PATH / "python-build-standalone-releases.json"
TEST_FAIL_CWD_FILE: Final[Path] = PATH / "testing_temp_dir_file.py"
# this value is cached because it's used a lot in unit tests
@functools.cache
def read_all_configs() -> dict[str, list[dict[str, str]]]:
with BUILD_PLATFORMS.open("rb") as f:
loaded_file = tomllib.load(f)
return {k: list[dict[str, str]](v["python_configurations"]) for k, v in loaded_file.items()}
def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
return read_all_configs()[config]
+165
View File
@@ -0,0 +1,165 @@
import contextlib
import functools
import os
import shutil
import sys
import tomllib
from collections.abc import Sequence
from pathlib import Path
from typing import Final
from filelock import FileLock
from packaging.requirements import InvalidRequirement, Requirement
from packaging.version import Version
from .util import resources
from .util.cmd import call
from .util.file import CIBW_CACHE_PATH, download
_IS_WIN: Final[bool] = sys.platform.startswith("win")
@functools.cache
def _ensure_virtualenv(version: str) -> tuple[Path, Version]:
version_parts = version.split(".")
key = f"py{version_parts[0]}{version_parts[1]}"
with resources.VIRTUALENV.open("rb") as f:
loaded_file = tomllib.load(f)
configuration = loaded_file.get(key, loaded_file["default"])
version = str(configuration["version"])
url = str(configuration["url"])
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
with FileLock(str(path) + ".lock"):
if not path.exists():
download(url, path)
return (path, Version(version))
def constraint_flags(
dependency_constraint: Path | None,
) -> Sequence[str]:
"""
Returns the flags to pass to pip for the given dependency constraint.
"""
return ["-c", dependency_constraint.as_uri()] if dependency_constraint else []
def _parse_pip_constraint_for_virtualenv(
constraint_path: Path | None,
) -> str:
"""
Parses the constraints file referenced by `dependency_constraint_flags` and returns a dict where
the key is the package name, and the value is the constraint version.
If a package version cannot be found, its value is "embed" meaning that virtualenv will install
its bundled version, already available locally.
The function does not try to be too smart and just handles basic constraints.
If it can't get an exact version, the real constraint will be handled by the
{macos|windows}.setup_python function.
"""
if constraint_path:
assert constraint_path.exists()
with constraint_path.open(encoding="utf-8") as constraint_file:
for line_ in constraint_file:
line = line_.strip()
if not line:
continue
if line.startswith("#"):
continue
try:
requirement = Requirement(line)
package = requirement.name
if (
package != "pip"
or requirement.url is not None
or requirement.marker is not None
or len(requirement.extras) != 0
or len(requirement.specifier) != 1
):
continue
specifier = next(iter(requirement.specifier))
if specifier.operator != "==":
continue
return specifier.version
except InvalidRequirement:
continue
return "embed"
def virtualenv(
version: str,
python: Path,
venv_path: Path,
dependency_constraint: Path | None,
*,
use_uv: bool,
env: dict[str, str] | None = None,
pip_version: str | None = None,
) -> dict[str, str]:
"""
Create a virtual environment. If `use_uv` is True,
dependency_constraint_flags are ignored since nothing is installed in the
venv. Otherwise, pip is installed.
"""
# virtualenv may fail if this is a symlink.
python = python.resolve()
assert python.exists()
if use_uv:
call("uv", "venv", venv_path, "--python", python)
else:
virtualenv_app, virtualenv_version = _ensure_virtualenv(version)
if pip_version is None:
pip_version = _parse_pip_constraint_for_virtualenv(dependency_constraint)
additional_flags = [f"--pip={pip_version}", "--no-setuptools"]
if virtualenv_version < Version("20.31") or Version(version) < Version("3.9"):
additional_flags.append("--no-wheel")
# Using symlinks to pre-installed seed packages is really the fastest way to get a virtual
# environment. The initial cost is a bit higher but reusing is much faster.
# Windows does not always allow symlinks so just disabling for now.
# Requires pip>=19.3 so disabling for "embed" because this means we don't know what's the
# version of pip that will end-up installed.
# c.f. https://virtualenv.pypa.io/en/latest/cli_interface.html#section-seeder
if not _IS_WIN and pip_version != "embed" and Version(pip_version) >= Version("19.3"):
additional_flags.append("--symlink-app-data")
call(
sys.executable,
"-sS", # just the stdlib, https://github.com/pypa/virtualenv/issues/2133#issuecomment-1003710125
virtualenv_app,
"--activators=",
"--no-periodic-update",
*additional_flags,
"--python",
python,
venv_path,
)
paths = [str(venv_path), str(venv_path / "Scripts")] if _IS_WIN else [str(venv_path / "bin")]
venv_env = os.environ.copy() if env is None else env.copy()
venv_env["PATH"] = os.pathsep.join([*paths, venv_env["PATH"]])
venv_env["VIRTUAL_ENV"] = str(venv_path)
if not use_uv and pip_version == "embed":
call(
"pip",
"install",
"--upgrade",
"pip",
*constraint_flags(dependency_constraint),
env=venv_env,
cwd=venv_path,
)
return venv_env
def find_uv() -> Path | None:
# Prefer uv in our environment
with contextlib.suppress(ImportError, FileNotFoundError):
from uv import find_uv_bin # noqa: PLC0415
return Path(find_uv_bin())
uv_on_path = shutil.which("uv")
return Path(uv_on_path) if uv_on_path else None
+65 -7
View File
@@ -4,11 +4,69 @@ title: Changelog
# Changelog
### v2.23.4
### v3.1.0
_16 March 2026_
_23 July 2025_
- 🐛 Fix HTTP 429 errors while downloading virtualenv from GitHub blob URLs uses a https://bootstrap.pypa.io/ URL instead. (#2775)
- 🌟 CPython 3.14 wheels are now built by default - without the `"cpython-prerelease"` `enable` set. It's time to build and upload these wheels to PyPI! This release includes CPython 3.14.0rc1, which is guaranteed to be ABI compatible with the final release. (#2507) Free-threading is no longer experimental in 3.14, so you have to skip it explicitly with `'cp31?t-*'` if you don't support it yet. (#2503)
- 🌟 Adds the ability to [build wheels for Android](https://cibuildwheel.pypa.io/en/stable/platforms/#android)! Set the [`platform` option](https://cibuildwheel.pypa.io/en/stable/options/#platform) to `android` on Linux or macOS to try it out! (#2349)
- 🌟 Adds Pyodide 0.28, which builds 3.13 wheels (#2487)
- ✨ Support for 32-bit `manylinux_2_28` (now a consistent default) and `manylinux_2_34` added (#2500)
- 🛠 Improved summary, will also use markdown summary output on GHA (#2469)
- 🛠 The riscv64 images now have a working default (as they are now part of pypy/manylinux), but are still experimental (and behind an `enable`) since you can't push them to PyPI yet (#2506)
- 🛠 Fixed a typo in the 3.9 MUSL riscv64 identifier (`cp39-musllinux_ricv64` -> `cp39-musllinux_riscv64`) (#2490)
- 🛠 Mistyping `--only` now shows the correct possibilities, and even suggests near matches on Python 3.14+ (#2499)
- 🛠 Only support one output from the repair step on linux like other platforms; auditwheel fixed this over four years ago! (#2478)
- 🛠 We now use pattern matching extensively (#2434)
- 📚 We now have platform maintainers for our special platforms and interpreters! (#2481)
### v3.0.1
_5 July 2025_
- 🛠 Updates CPython 3.14 prerelease to 3.14.0b3 (#2471)
- ✨ Adds a CPython 3.14 prerelease iOS build (only when prerelease builds are [enabled](https://cibuildwheel.pypa.io/en/stable/options/#enable)) (#2475)
### v3.0.0
_11 June 2025_
See @henryiii's [release post](https://iscinumpy.dev/post/cibuildwheel-3-0-0/) for more info on new features!
- 🌟 Adds the ability to [build wheels for iOS](https://cibuildwheel.pypa.io/en/stable/platforms/#ios)! Set the [`platform` option](https://cibuildwheel.pypa.io/en/stable/options/#platform) to `ios` on a Mac with the iOS toolchain to try it out! (#2286, #2363, #2432)
- 🌟 Adds support for the GraalPy interpreter! Enable for your project using the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable). (#1538, #2411, #2414)
- ✨ Adds CPython 3.14 support, under the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable) `cpython-prerelease`. This version of cibuildwheel uses 3.14.0b2. (#2390)
_While CPython is in beta, the ABI can change, so your wheels might not be compatible with the final release. For this reason, we don't recommend distributing wheels until RC1, at which point 3.14 will be available in cibuildwheel without the flag._ (#2390)
- ✨ Adds the [test-sources option](https://cibuildwheel.pypa.io/en/stable/options/#test-sources), which copies files and folders into the temporary working directory we run tests from. (#2062, #2284, #2420, #2437)
This is particularly important for iOS builds, which do not support placeholders in the `test-command`, but can also be useful for other platforms.
- ✨ Adds [`dependency-versions`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) inline syntax (#2122)
- ✨ Improves support for Pyodide builds and adds the experimental [`pyodide-version`](https://cibuildwheel.pypa.io/en/stable/options/#pyodide-version) option, which allows you to specify the version of Pyodide to use for builds. (#2002)
- ✨ Add `pyodide-prerelease` [enable](https://cibuildwheel.pypa.io/en/stable/options/#enable) option, with an early build of 0.28 (Python 3.13). (#2431)
- ✨ Adds the [`test-environment`](https://cibuildwheel.pypa.io/en/stable/options/#test-environment) option, which allows you to set environment variables for the test command. (#2388)
- ✨ Adds the [`xbuild-tools`](https://cibuildwheel.pypa.io/en/stable/options/#xbuild-tools) option, which allows you to specify tools safe for cross-compilation. Currently only used on iOS; will be useful for Android in the future. (#2317)
- 🛠 The default [manylinux image](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) has changed from `manylinux2014` to `manylinux_2_28`. (#2330)
- 🛠 EOL images `manylinux1`, `manylinux2010`, `manylinux_2_24` and `musllinux_1_1` can no longer be specified by their shortname. The full OCI name can still be used for these images, if you wish. (#2316)
- 🛠 Invokes `build` rather than `pip wheel` to build wheels by default. You can control this via the [`build-frontend`](https://cibuildwheel.pypa.io/en/stable/options/#build-frontend) option. You might notice that you can see your build log output now! (#2321)
- 🛠 Build verbosity settings have been reworked to have consistent meanings between build backends when non-zero. (#2339)
- 🛠 Removed the `CIBW_PRERELEASE_PYTHONS` and `CIBW_FREE_THREADED_SUPPORT` options - these have been folded into the [`enable`](https://cibuildwheel.pypa.io/en/stable/options/#enable) option instead. (#2095)
- 🛠 Build environments no longer have setuptools and wheel preinstalled. (#2329)
- 🛠 Use the standard Schema line for the integrated JSONSchema. (#2433)
- ⚠️ Dropped support for building Python 3.6 and 3.7 wheels. If you need to build wheels for these versions, use cibuildwheel v2.23.3 or earlier. (#2282)
- ⚠️ The minimum Python version required to run cibuildwheel is now Python 3.11. You can still build wheels for Python 3.8 and newer. (#1912)
- ⚠️ 32-bit Linux wheels no longer built by default - the [arch](https://cibuildwheel.pypa.io/en/stable/options/#archs) was removed from `"auto"`. It now requires explicit `"auto32"`. Note that modern manylinux images (like the new default, `manylinux_2_28`) do not have 32-bit versions. (#2458)
- ⚠️ PyPy wheels no longer built by default, due to a change to our options system. To continue building PyPy wheels, you'll now need to set the [`enable` option](https://cibuildwheel.pypa.io/en/stable/options/#enable) to `pypy` or `pypy-eol`. (#2095)
- ⚠️ Dropped official support for Appveyor. If it was working for you before, it will probably continue to do so, but we can't be sure, because our CI doesn't run there anymore. (#2386)
- 📚 A reorganisation of the docs, and numerous updates. (#2280)
- 📚 Use Python 3.14 color output in docs CLI output. (#2407)
- 📚 Docs now primarily use the pyproject.toml name of options, rather than the environment variable name. (#2389)
- 📚 README table now matches docs and auto-updates. (#2427, #2428)
### v2.23.3
@@ -191,7 +249,7 @@ _12 May 2024_
_11 March 2024_
- 🌟 Adds the ability to inherit configuration in TOML overrides. This makes certain configurations much simpler. If you're overriding an option like `before-build` or `environment`, and you just want to add an extra command or environment variable, you can just append (or prepend) to the previous config. See [the docs](https://cibuildwheel.pypa.io/en/stable/options/#inherit) for more information. (#1730)
- 🌟 Adds official support for native `arm64` macOS GitHub runners. To use them, just specify `macos-latest` as an `os` of your job in your workflow file. You can also keep `macos-15-intel` in your build matrix to build `x86_64`. Check out the new [GitHub Actions example config](https://cibuildwheel.pypa.io/en/stable/setup/#github-actions).
- 🌟 Adds official support for native `arm64` macOS GitHub runners. To use them, just specify `macos-14` as an `os` of your job in your workflow file. You can also keep `macos-13` in your build matrix to build `x86_64`. Check out the new [GitHub Actions example config](https://cibuildwheel.pypa.io/en/stable/setup/#github-actions).
- ✨ You no longer need to specify `--platform` to run cibuildwheel locally! Instead it will detect your platform automatically. This was a safety feature, no longer necessary. (#1727)
- 🛠 Removed setuptools and wheel pinned versions. This only affects old-style projects without a `pyproject.toml`, projects with `pyproject.toml` are already getting fresh versions of their `build-system.requires` installed into an isolated environment. (#1725)
- 🛠 Improve how the GitHub Action passes arguments (#1757)
@@ -208,7 +266,7 @@ _11 March 2024_
_30 January 2024_
- 🐛 Fix an incompatibility with the GitHub Action and new GitHub Runner images for Windows that bundle Powershell 7.3+ (#1741)
- 🛠 Preliminary support for new `macos-latest` arm64 runners (#1743)
- 🛠 Preliminary support for new `macos-14` arm64 runners (#1743)
### v2.16.4
@@ -484,7 +542,7 @@ _14 December 2021_
_26 November 2021_
- 📈 cibuildwheel now defaults to manylinux2014 image for linux builds, rather than manylinux2010. If you want to stick with manylinux2010, it's simple to set this using [the image options](https://cibuildwheel.pypa.io/en/stable/options/#linux-image). (#926)
- ✨ You can now pass environment variables from the host machine into the Docker container during a Linux build. Check out [the docs for `CIBW_ENVIRONMENT_PASS_LINUX `](https://cibuildwheel.pypa.io/en/latest/options/#environment-pass) for the details. (#914)
- ✨ You can now pass environment variables from the host machine into the Docker container during a Linux build. Check out [the docs for `CIBW_ENVIRONMENT_PASS_LINUX `](https://cibuildwheel.pypa.io/en/stable/options/#environment-pass) for the details. (#914)
- ✨ Added support for building PyPy 3.8 wheels. (#881)
- ✨ Added support for building Windows arm64 CPython wheels on a Windows arm64 runner. We can't test this in CI yet, so for now, this is experimental. (#920)
- 📚 Improved the deployment documentation (#911)
@@ -515,7 +573,7 @@ _22 October 2021_
- 🌟 TOML option overrides! This provides much greater flexibility in configuration via pyproject.toml. (#854)
You can now set build options for any subset of your builds using a match pattern. So, for example, you can customise CPython 3.8 builds with an override on `cp38-*` or musllinux builds by selecting `*musllinux*`. Check out [the docs](https://cibuildwheel.pypa.io/en/latest/options/#overrides) for more info on the specifics.
You can now set build options for any subset of your builds using a match pattern. So, for example, you can customise CPython 3.8 builds with an override on `cp38-*` or musllinux builds by selecting `*musllinux*`. Check out [the docs](https://cibuildwheel.pypa.io/en/stable/options/#overrides) for more info on the specifics.
- 🛠 Added support for building PyPy wheels on macOS 11 CI runners. (#875)
+175
View File
@@ -0,0 +1,175 @@
---
title: Configuring a CI service
---
## Configuring a CI service
cibuildwheel works on many popular CI services. Others may work, but it will depend on the software installed on the CI machine/image. See the [platforms page](platforms.md) for details.
### GitHub Actions [linux/mac/windows] {: #github-actions}
To build Linux, macOS, and Windows wheels using GitHub Actions, create a `.github/workflows/build_wheels.yml` file in your repo.
!!! tab "Action"
For GitHub Actions, `cibuildwheel` provides an action you can use. This is
concise and enables easier auto updating via GitHub's Dependabot; see
[Automatic updates](faq.md#automatic-updates).
> .github/workflows/build_wheels.yml
```yaml
{% include "../examples/github-minimal.yml" %}
```
Use `env:` to pass [build options](options.md) and `with:` to set
`package-dir: .`, `output-dir: wheelhouse` and `config-file: ''`
locations (those values are the defaults).
!!! tab "pipx"
The GitHub Actions runners have pipx installed, so you can easily build in
just one line. This is internally how the action works; the main benefit of
the action form is easy updates via GitHub's Dependabot.
> .github/workflows/build_wheels.yml
```yaml
{% include "../examples/github-pipx.yml" %}
```
!!! tab "Generic"
This is the most generic form using setup-python and pip; it looks the most
like the other CI examples. If you want to avoid having setup that takes
advantage of GitHub Actions features or pipx being preinstalled, this might
appeal to you.
> .github/workflows/build_wheels.yml
{%
include-markdown "../README.md"
start="<!--generic-github-start-->"
end="<!--generic-github-end-->"
%}
Commit this file, and push to GitHub - either to your default branch, or to a PR branch. The build should start automatically.
For more info on this file, check out the [docs](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions).
[`examples/github-deploy.yml`](https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml) extends this minimal example to include Android, iOS and Pyodide builds, and a demonstration of how to automatically upload the built wheels to PyPI.
### Azure Pipelines [linux/mac/windows] {: #azure-pipelines}
To build Linux, Mac, and Windows wheels on Azure Pipelines, create a `azure-pipelines.yml` file in your repo.
> azure-pipelines.yml
```yaml
{% include "../examples/azure-pipelines-minimal.yml" %}
```
Commit this file, enable building of your repo on Azure Pipelines, and push.
Wheels will be stored for you and available through the Pipelines interface. For more info on this file, check out the [docs](https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema).
### Travis CI [linux/windows] {: #travis-ci}
To build Linux and Windows wheels on Travis CI, create a `.travis.yml` file in your repo.
> .travis.yml
```yaml
{% include "../examples/travis-ci-minimal.yml" %}
```
Commit this file, enable building of your repo on Travis CI, and push.
Then setup a deployment method by following the [Travis CI deployment docs](https://docs.travis-ci.com/user/deployment/), or see [Delivering to PyPI](deliver-to-pypi.md). For more info on `.travis.yml`, check out the [docs](https://docs.travis-ci.com/).
[`examples/travis-ci-deploy.yml`](https://github.com/pypa/cibuildwheel/blob/main/examples/travis-ci-deploy.yml) extends this minimal example with a demonstration of how to automatically upload the built wheels to PyPI.
### CircleCI [linux/mac] {: #circleci}
To build Linux and Mac wheels on CircleCI, create a `.circleci/config.yml` file in your repo,
> .circleci/config.yml
```yaml
{% include "../examples/circleci-minimal.yml" %}
```
Commit this file, enable building of your repo on CircleCI, and push.
!!! note
CircleCI doesn't enable free macOS containers for open source by default, but you can ask for access. See [here](https://circleci.com/docs/2.0/oss/#overview) for more information.
CircleCI will store the built wheels for you - you can access them from the project console. Check out the CircleCI [docs](https://circleci.com/docs/2.0/configuration-reference/#section=configuration) for more info on this config file.
### Gitlab CI [linux] {: #gitlab-ci}
To build Linux wheels on Gitlab CI, create a `.gitlab-ci.yml` file in your repo,
> .gitlab-ci.yml
```yaml
{% include "../examples/gitlab-minimal.yml" %}
```
Commit this file, and push to Gitlab. The pipeline should start automatically.
Gitlab will store the built wheels for you - you can access them from the Pipelines view. Check out the Gitlab [docs](https://docs.gitlab.com/ee/ci/yaml/) for more info on this config file.
### Cirrus CI [linux/mac/windows] {: #cirrus-ci}
To build Linux, Mac, and Windows wheels on Cirrus CI, create a `.cirrus.yml` file in your repo,
> .cirrus.yml
```yaml
{% include "../examples/cirrus-ci-minimal.yml" %}
```
Commit this file, enable building of your repo on Cirrus CI, and push.
Cirrus CI will store the built wheels for you - you can access them from the individual task view. Check out the Cirrus CI [docs](https://cirrus-ci.org/guide/writing-tasks/) for more info on this config file.
> ⚠️ Got an error? Check the [FAQ](faq.md).
### Other CI services
#### AppVeyor {: #appveyor}
Appveyor official support was dropped in cibuildwheel v3.0, due to a lack of CI credits. However, it can probably still be used as-is. Check the Appveyor example from the cibuildwheel v2.0 branch: [appveyor-minimal.yml](https://github.com/pypa/cibuildwheel/blob/v2.23.3/examples/appveyor-minimal.yml).
## Next steps
Once you've got the wheel building successfully, you might want to set up [testing](options.md#test-command) or [automatic releases to PyPI](deliver-to-pypi.md#automatic-method).
<script>
document.addEventListener('DOMContentLoaded', function() {
$('.toctree-l2>a, .rst-content h3').each(function(i, el) {
var text = $(el).text()
var match = text.match(/(.*) \[([a-z/]+)\]/);
if (match) {
var iconHTML = $.map(match[2].split('/'), function(ident) {
switch (ident) {
case 'linux':
return '<i class="fa fa-linux" aria-hidden="true"></i>'
case 'windows':
return '<i class="fa fa-windows" aria-hidden="true"></i>'
case 'mac':
return '<i class="fa fa-apple" aria-hidden="true"></i>'
}
}).join(' ');
$(el).append(
$('<div>')
.append(iconHTML)
.css({float: 'right'})
)
$(el).contents()
.filter(function(){ return this.nodeType == 3; }).first()
.replaceWith(match[1]);
}
});
});
</script>
+227
View File
@@ -0,0 +1,227 @@
# Configuration methods
cibuildwheel can either be configured using environment variables, or from
config file such as `pyproject.toml`.
This page describes how to set options. For a full list of available options, see the [options reference](options.md).
## Environment variables {: #environment-variables}
Environment variables can be set in your CI config. For example, to configure
cibuildwheel to run tests, add the following YAML to your CI config file:
!!! tab "GitHub Actions"
> .github/workflows/*.yml ([docs](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables)) (can be global, in job, or in step)
```yaml
env:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
!!! tab "Azure Pipelines"
> azure-pipelines.yml ([docs](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables))
```yaml
variables:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
!!! tab "Travis CI"
> .travis.yml ([docs](https://docs.travis-ci.com/user/environment-variables/))
```yaml
env:
global:
- CIBW_TEST_REQUIRES=pytest
- CIBW_TEST_COMMAND="pytest {project}/tests"
```
!!! tab "CircleCI"
> .circleci/config.yml ([docs](https://circleci.com/docs/2.0/configuration-reference/#environment))
```yaml
jobs:
job_name:
environment:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
!!! tab "Gitlab CI"
> .gitlab-ci.yml ([docs](https://docs.gitlab.com/ci/yaml/#variables))
```yaml
linux:
variables:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
!!! tab "Cirrus CI"
> .cirrus.yml ([docs](https://cirrus-ci.org/guide/writing-tasks/#environment-variables))
```yaml
env:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
## Configuration file {: #configuration-file}
You can configure cibuildwheel with a config file, such as `pyproject.toml`.
Options have the same names as the environment variable overrides, but are
placed in `[tool.cibuildwheel]` and are lower case, with dashes, following
common [TOML][https://toml.io] practice. Anything placed in subsections `linux`, `windows`,
`macos`, or `pyodide` will only affect those platforms. Lists can be used
instead of strings for items that are naturally a list. Multiline strings also
work just like in the environment variables. Environment variables will take
precedence if defined.
The example above using environment variables could have been written like this:
```toml
[tool.cibuildwheel]
test-requires = "pytest"
test-command = "pytest ./tests"
```
The complete set of defaults for the current version of cibuildwheel are shown below:
```toml
{% include "../cibuildwheel/resources/defaults.toml" %}
```
!!! tip
Static configuration works across all CI systems, and can be used locally if
you run `cibuildwheel --platform linux`. This is preferred, but environment
variables are better if you need to change per-matrix element
(`CIBW_BUILD` is often in this category, for example), or if you cannot or do
not want to change a `pyproject.toml` file. You can specify a different file to
use with `--config-file` on the command line, as well.
## Configuration overrides {: #overrides }
One feature specific to the configuration files is the ability to override
settings based on selectors. To use, add a ``tool.cibuildwheel.overrides``
array, and specify a ``select`` string. Then any options you set will only
apply to items that match that selector. These are applied in order, with later
matches overriding earlier ones if multiple selectors match. Environment
variables always override static configuration.
A few of the options below have special handling in overrides. A different
`before-all` will trigger a new container to launch on Linux, and cannot be
overridden on macOS or Windows. Overriding the image on linux will also
trigger new containers, one per image. Some commands are not supported;
`output-dir`, build/skip/test_skip selectors, and architectures cannot be
overridden.
You can specify a table of overrides in `inherit={}`, any list or table in this
list will inherit from previous overrides or the main configuration. The valid
options are `"none"` (the default), `"append"`, and `"prepend"`.
#### Examples:
```toml
[tool.cibuildwheel.linux]
before-all = "yum install mylib"
test-command = "echo 'installed'"
[[tool.cibuildwheel.overrides]]
select = "*-musllinux*"
before-all = "apk add mylib"
```
This example will override the before-all command on musllinux only, but will
still run the test-command. Note the double brackets, this is an array in TOML,
which means it can be given multiple times.
```toml
[tool.cibuildwheel]
# Normal options, etc.
manylinux-x86_64-image = "manylinux_2_34"
[[tool.cibuildwheel.overrides]]
select = "cp38-*"
manylinux-x86_64-image = "manylinux2014"
[[tool.cibuildwheel.overrides]]
select = "cp3{9,10}-*"
manylinux-x86_64-image = "manylinux_2_28"
```
This example will build CPython 3.8 wheels on manylinux2014, CPython 3.9-3.10
wheels on manylinux_2_28, and manylinux_2_34 wheels for any newer Python
(like 3.10).
```toml
[tool.cibuildwheel]
environment = {FOO="BAR", "HAM"="EGGS"}
test-command = ["pyproject"]
[[tool.cibuildwheel.overrides]]
select = "cp311*"
inherit.test-command = "prepend"
test-command = ["pyproject-before"]
inherit.environment="append"
environment = {FOO="BAZ", "PYTHON"="MONTY"}
[[tool.cibuildwheel.overrides]]
select = "cp311*"
inherit.test-command = "append"
test-command = ["pyproject-after"]
```
This example will provide the command `"pyproject-before && pyproject && pyproject-after"`
on Python 3.11, and will have `environment = {FOO="BAZ", "PYTHON"="MONTY", "HAM"="EGGS"}`.
## Extending existing options {: #inherit }
In the TOML configuration, you can choose how tables and lists are inherited.
By default, all values are overridden completely (`"none"`) but sometimes you'd
rather `"append"` or `"prepend"` to an existing list or table. You can do this
with the `inherit` table in overrides. For example, if you want to add an environment
variable for CPython 3.11, without `inherit` you'd have to repeat all the
original environment variables in the override. With `inherit`, it's just:
```toml
[[tool.cibuildwheel.overrides]]
select = "cp311*"
inherit.environment = "append"
environment.NEWVAR = "Added!"
```
For a table, `"append"` will replace a key if it exists, while `"prepend"` will
only add a new key, older keys take precedence.
Lists are also supported (and keep in mind that commands are lists). For
example, you can print a message before and after a wheel is repaired:
```toml
[[tool.cibuildwheel.overrides]]
select = "*"
inherit.repair-wheel-command = "prepend"
repair-wheel-command = "echo 'Before repair'"
[[tool.cibuildwheel.overrides]]
select = "*"
inherit.repair-wheel-command = "append"
repair-wheel-command = "echo 'After repair'"
```
As seen in this example, you can have multiple overrides match - they match top
to bottom, with the config being accumulated. If you need platform-specific
inheritance, you can use `select = "*-????linux_*"` for Linux, `select =
"*-win_*"` for Windows, and `select = "*-macosx_*"` for macOS. As always,
environment variables will completely override any TOML configuration.
+5 -3
View File
@@ -16,7 +16,7 @@ Everyone contributing to the cibuildwheel project is expected to follow the [PSF
- `cibuildwheel` should wrap the complexity of wheel building.
- The user interface to `cibuildwheel` is the build script (e.g. `.travis.yml`). Feature additions should not increase the complexity of this script.
- Options should be environment variables (these lend themselves better to YML config files). They should be prefixed with `CIBW_`.
- Options should be environment variables (these lend themselves better to YAML config files). They should be prefixed with `CIBW_`.
- Options should be generalised to all platforms. If platform-specific options are required, they should be namespaced e.g. `CIBW_TEST_COMMAND_MACOS`
Other notes:
@@ -36,7 +36,7 @@ So, if we can, I'd like to improve the experience on errors as well. In [this](h
### Running the tests
When making a change to the codebase, you can run tests locally for quicker feedback than the CI runs on a PR. You can [run them directly](#making-a-venv), but the easiest way to run tests is using [nox](https://nox.thea.codes/).
When making a change to the codebase, you can run tests locally for quicker feedback than the CI runs on a PR. You can run them directly, but the easiest way to run tests is using [nox](https://nox.thea.codes/).
You can run all the tests locally by doing:
@@ -78,10 +78,12 @@ nox -s tests -- test -k before_build
A few notes-
- Because they run inside a container, Linux tests can run on all platforms where Docker is installed, so they're convenient for running integration tests locally. Set CIBW_PLATFORM to do this: `CIBW_PLATFORM=linux nox -s tests -- test`.
- Because they run inside a container, Linux tests can run on all platforms where Docker is installed, so they're convenient for running integration tests locally. Set the `--platform` flag on pytest to do this: `nox -s tests -- test --platform linux`.
- Running the macOS integration tests requires _system installs_ of Python from python.org for all the versions that are tested. We won't attempt to install these when running locally, but you can do so manually using the URL in the error message that is printed when the install is not found.
- The ['enable groups'](options.md#enable) run by default are just 'cpython-prerelease' and 'cpython-freethreading'. You can add other groups like pypy or graalpy by passing the `--enable` argument to pytest, i.e. `nox -s tests -- test --enable pypy`. On GitHub PRs, you can add a label to the PR to enable these groups.
#### Running pytest directly
More advanced users might prefer to invoke pytest directly. Set up a [dev environment](#setting-up-a-dev-environment), then,
+5 -5
View File
@@ -7,18 +7,18 @@ title: Modern C++ standards
Building Python wheels with modern C++ standards (C++11 and later) requires a few tricks.
## manylinux1 and C++14
The past end-of-life `manylinux1` image (based on CentOS 5) contains a version of GCC and libstdc++ that only supports C++11 and earlier standards. There are however ways to compile wheels with the C++14 standard (and later): https://github.com/pypa/manylinux/issues/118
## manylinux2014 and C++20
`manylinux2010` and `manylinux2014` are newer and support all C++ standards (up to C++17).
The past end-of-life `manylinux2014` image (based on CentOS 7) contains a version of GCC and libstdc++ that only supports C++17 and earlier standards.
`manylinux_2_28` are newer and support all C++ standards (up to C++20).
## macOS and deployment target versions
OS X/macOS allows you to specify a so-called "deployment target" version that will ensure backwards compatibility with older versions of macOS. One way to do this is by setting the `MACOSX_DEPLOYMENT_TARGET` environment variable.
The [`MACOSX_DEPLOYMENT_TARGET` environment variable](platforms.md#macos-version-compatibility) is used to set the minimum deployment target for macOS.
However, to enable modern C++ standards, the deployment target needs to be set high enough (since older OS X/macOS versions did not have the necessary modern C++ standard library).
To get C++17 support, Xcode 9.3+ is needed, requiring at least macOS 10.13 on the build machine. To use C++17 library features and link against the C++ runtime library, set `MACOSX_DEPLOYMENT_TARGET` to `"10.13"` or `"10.14"` (or higher) - macOS 10.13 offers partial C++17 support (e.g., the filesystem header is in experimental, offering `#include <experimental/filesystem>` instead of `#include <filesystem>`); macOS 10.14 has full C++17 support. CPython 3.12+ require 10.13+ anyway.
However, if only C++17 compiler and standard template library (STL) features are used (not needing a C++17 runtime) it might be possible to set `MACOSX_DEPLOYMENT_TARGET` to a lower value, such as `"10.9"`. To find out if this is the case, try compiling and running with a lower `MACOSX_DEPLOYMENT_TARGET`: if C++17 features are used that require a more recent deployment target, building the wheel should fail.

Some files were not shown because too many files have changed in this diff Show More