Merge remote-tracking branch 'origin/master' into python3.9

This commit is contained in:
Joe Rickerby
2020-07-24 15:35:31 +01:00
46 changed files with 1246 additions and 524 deletions
+3 -1
View File
@@ -1,6 +1,8 @@
set -o errexit
set -o xtrace
$PYTHON --version $PYTHON --version
$PYTHON -m pip --version $PYTHON -m pip --version
$PYTHON -m pip install -q --user --ignore-installed --upgrade virtualenv
$PYTHON -m virtualenv -p $PYTHON venv $PYTHON -m virtualenv -p $PYTHON venv
venv/bin/python -m pip install -r requirements-dev.txt venv/bin/python -m pip install -r requirements-dev.txt
venv/bin/python -m pip freeze venv/bin/python -m pip freeze
+7
View File
@@ -0,0 +1,7 @@
# config file for https://github.com/bobvanderlinden/probot-auto-merge
# no approvals required, just the label is enough
minApprovals: {}
requiredLabels:
- merge when green
+1
View File
@@ -17,6 +17,7 @@ jobs:
matrix: matrix:
os: [ubuntu-18.04, windows-latest, macos-latest] os: [ubuntu-18.04, windows-latest, macos-latest]
python_version: ['3.7'] python_version: ['3.7']
timeout-minutes: 180
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
+21
View File
@@ -0,0 +1,21 @@
repos:
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.1
hooks:
- id: flake8
pass_filenames: true
# this seems to need to be here in addition to setup.cfg
exclude: cibuildwheel/resources/
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: check-merge-conflict
- id: end-of-file-fixer
exclude: cibuildwheel/resources/pinned_docker_images.cfg
- repo: https://github.com/pre-commit/mirrors-mypy
rev: '' # Use the sha / tag you want to point at
hooks:
- id: mypy
files: cibuildwheel|test
exclude: cibuildwheel/resources
+78 -17
View File
@@ -22,11 +22,13 @@ What does it do?
| CPython 3.6 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | | CPython 3.6 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² |
| CPython 3.7 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | | CPython 3.7 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² |
| CPython 3.8 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | | CPython 3.8 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² |
| CPython 3.9³ | 🛠 | 🛠 | 🛠 | 🛠 | 🛠 | 🛠 | 🛠 | |
| PyPy 2.7 v7.3.0 | ✅ | | ✅ | ✅ | | | | | | PyPy 2.7 v7.3.0 | ✅ | | ✅ | ✅ | | | | |
| PyPy 3.6 v7.3.0 | ✅ | | ✅ | ✅ | | | | | | PyPy 3.6 v7.3.0 | ✅ | | ✅ | ✅ | | | | |
<sup>¹ Not supported on Travis</sup><br> <sup>¹ Not supported on Travis</sup><br>
<sup>² Beta support until Travis CI fixes <a href="https://travis-ci.community/t/no-space-left-on-device-for-system-z/5954/11">a bug</a></sup> <sup>² Beta support until Travis CI fixes <a href="https://travis-ci.community/t/no-space-left-on-device-for-system-z/5954/11">a bug</a></sup><br>
<sup>³ Python 3.9 is not yet ABI stable, so you shouldn't publish wheels with it yet. But if you want to check that your wheels build on Python 3.9, try our [`python3.9` branch](https://github.com/joerick/cibuildwheel/pull/382)!</sup>
- Builds manylinux, macOS and Windows wheels for CPython and PyPy using Azure Pipelines, Travis CI, AppVeyor, and CircleCI - Builds manylinux, macOS and Windows wheels for CPython and PyPy using Azure Pipelines, Travis CI, AppVeyor, and CircleCI
- Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate) - Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate)
@@ -77,7 +79,7 @@ env:
# Note: TWINE_PASSWORD is set to a PyPI API token in Travis settings # Note: TWINE_PASSWORD is set to a PyPI API token in Travis settings
install: install:
- python3 -m pip install cibuildwheel==1.4.2 - python3 -m pip install cibuildwheel==1.5.5
script: script:
# build the wheels, put them into './wheelhouse' # build the wheels, put them into './wheelhouse'
@@ -92,7 +94,7 @@ after_success:
fi fi
``` ```
For more information, including how to build on Appveyor, Azure, CircleCI, check out the [documentation](https://cibuildwheel.readthedocs.org) and also check out [the examples](https://github.com/joerick/cibuildwheel/tree/master/examples). For more information, including how to build on GitHub Actions, Appveyor, Azure Pipelines, or CircleCI, check out the [documentation](https://cibuildwheel.readthedocs.org) and the [examples](https://github.com/joerick/cibuildwheel/tree/master/examples).
Options Options
------- -------
@@ -100,17 +102,20 @@ Options
| | Option | Description | | | Option | Description |
|---|--------|-------------| |---|--------|-------------|
| **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.readthedocs.io/en/stable/options/#platform) | Override the auto-detected target platform | | **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.readthedocs.io/en/stable/options/#platform) | Override the auto-detected target platform |
| | [`CIBW_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) [`CIBW_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) | Choose the Python versions to build | | | [`CIBW_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) <br> [`CIBW_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) | Choose the Python versions to build |
| **Build environment** | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build | | **Build customization** | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build | | | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each (non-pure Python) built wheel | | | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each (non-pure Python) built wheel |
| | [`CIBW_MANYLINUX_X86_64_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) [`CIBW_MANYLINUX_I686_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) [`CIBW_MANYLINUX_PYPY_X86_64_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) | Specify alternative manylinux docker images | | | [`CIBW_MANYLINUX_X86_64_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) <br> [`CIBW_MANYLINUX_I686_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) <br> [`CIBW_MANYLINUX_PYPY_X86_64_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) <br> [`CIBW_MANYLINUX_AARCH64_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) <br> [`CIBW_MANYLINUX_PPC64LE_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) <br> [`CIBW_MANYLINUX_S390X_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) | Specify alternative manylinux docker images |
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel | | **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute shell command to prepare test environment | | | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests | | | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require | | | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
| **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel | | **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel |
Working examples Working examples
---------------- ----------------
@@ -147,6 +152,62 @@ This is similar to static linking, so it might have some licence implications. C
Changelog Changelog
========= =========
### 1.5.5
_22 July 2020_
- 🐛 Fix a bug that would cause command substitutions in CIBW_ENVIRONMENT to
produce no output on Linux (#411)
- 🐛 Fix regression (introduced in 1.5.3) which caused BEFORE_BUILD and
BEFORE_ALL to be executed in the wrong directory (#410)
### 1.5.4
_19 June 2020_
- 🐛 Fix a bug that would cause command substitutions in CIBW_ENVIRONMENT
variables to not interpret quotes in commands correctly (#406, #408)
### 1.5.3
_19 July 2020_
- 🛠 Update CPython 3.8 to 3.8.3 (#405)
- 🛠 Internal refactoring of Linux build, to move control flow into Python (#386)
### 1.5.2
_8 July 2020_
- 🐛 Fix an issue on Windows where pyproject.toml would cause an error when
some requirements formats were used. (#401)
- 🛠 Update CPython 3.7 to 3.7.8 (#394)
### 1.5.1
_25 June 2020_
- 🐛 Fix "OSError: [WinError 17] The system cannot move the file to a different
disk drive" on Github Actions (#388, #389)
### 1.5.0
_24 June 2020_
- 🌟 Add [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all)
option, which lets you run a command on the build machine before any wheels
are built. This is especially useful when building on Linux, to `make`
something external to Python, or to `yum install` a dependency. (#342)
- ✨ Added support for projects using pyproject.toml instead of setup.py
(#360, #358)
- ✨ Added workaround to allow Python 3.5 on Windows to pull dependencies from
pyproject.toml. (#358)
- 📚 Improved Github Actions examples and docs (#354, #362)
- 🐛 Ensure pip wheel uses the specified package, and doesn't build a wheel
from PyPI (#369)
- 🛠 Internal changes: using pathlib.Path, precommit hooks, testing
improvements.
### 1.4.2 ### 1.4.2
_25 May 2020_ _25 May 2020_
@@ -169,7 +230,7 @@ _4 May 2020_
_2 May 2020_ _2 May 2020_
- Deterministic builds. cibuildwheel now locks the versions of the tools it - 🌟 Deterministic builds. cibuildwheel now locks the versions of the tools it
uses. This means that pinning your version of cibuildwheel pins the versions uses. This means that pinning your version of cibuildwheel pins the versions
of pip, setuptools, manylinux etc. that are used under the hood. This should of pip, setuptools, manylinux etc. that are used under the hood. This should
make things more reliable. But note that we don't control the entire build make things more reliable. But note that we don't control the entire build
@@ -199,7 +260,7 @@ _2 May 2020_
_12 March 2020_ _12 March 2020_
- Add support for building on Github Actions! Check out the - 🌟 Add support for building on Github Actions! Check out the
[docs](https://cibuildwheel.readthedocs.io/en/stable/setup/#github-actions) [docs](https://cibuildwheel.readthedocs.io/en/stable/setup/#github-actions)
for information on how to set it up. (#194) for information on how to set it up. (#194)
- ✨ Add the `CIBW_BEFORE_TEST` option, which lets you run a command to - ✨ Add the `CIBW_BEFORE_TEST` option, which lets you run a command to
@@ -209,9 +270,9 @@ _12 March 2020_
_8 March 2020_ _8 March 2020_
- Add support for building PyPy wheels, across Manylinux, macOS, and - 🌟 Add support for building PyPy wheels, across Manylinux, macOS, and
Windows. (#185) Windows. (#185)
- Added the ability to build ARM64 (aarch64), ppc64le, and s390x wheels, - 🌟 Added the ability to build ARM64 (aarch64), ppc64le, and s390x wheels,
using manylinux2014 and Travis CI. (#273) using manylinux2014 and Travis CI. (#273)
- ✨ You can now build macOS wheels on Appveyor. (#230) - ✨ You can now build macOS wheels on Appveyor. (#230)
- 🛠 Changed default macOS minimum target to 10.9, from 10.6. This allows the - 🛠 Changed default macOS minimum target to 10.9, from 10.6. This allows the
@@ -232,7 +293,7 @@ _8 March 2020_
_7 December 2019_ _7 December 2019_
- Add support for building manylinux2014 wheels. To use, set - 🌟 Add support for building manylinux2014 wheels. To use, set
`CIBW_MANYLINUX_X86_64_IMAGE` and CIBW_MANYLINUX_I686_IMAGE to `CIBW_MANYLINUX_X86_64_IMAGE` and CIBW_MANYLINUX_I686_IMAGE to
`manylinux2014`. `manylinux2014`.
- ✨ Add support for [Linux on Appveyor](https://www.appveyor.com/blog/2018/03/06/appveyor-for-linux/) (#204, #207) - ✨ Add support for [Linux on Appveyor](https://www.appveyor.com/blog/2018/03/06/appveyor-for-linux/) (#204, #207)
@@ -245,8 +306,8 @@ _7 December 2019_
_10 November 2019_ _10 November 2019_
- Add support for building Python 3.8 wheels! (#180) - 🌟 Add support for building Python 3.8 wheels! (#180)
- Add support for building manylinux2010 wheels. cibuildwheel will now - 🌟 Add support for building manylinux2010 wheels. cibuildwheel will now
build using the manylinux2010 images by default. If your project is still build using the manylinux2010 images by default. If your project is still
manylinux1 compatible, you should get both manylinux1 and manylinux2010 manylinux1 compatible, you should get both manylinux1 and manylinux2010
wheels - you can upload both to PyPI. If you always require manylinux1 wheels, you can wheels - you can upload both to PyPI. If you always require manylinux1 wheels, you can
@@ -282,7 +343,7 @@ _28 May 2019_
_26 May 2019_ _26 May 2019_
- Add support for building on Azure pipelines! This lets you build all - 🌟 Add support for building on Azure pipelines! This lets you build all
Linux, Mac and Windows wheels on one service, so it promises to be the Linux, Mac and Windows wheels on one service, so it promises to be the
easiest to set up! Check out the quickstart in the docs, or easiest to set up! Check out the quickstart in the docs, or
[cibuildwheel-azure-example](https://github.com/joerick/cibuildwheel-azure-example) [cibuildwheel-azure-example](https://github.com/joerick/cibuildwheel-azure-example)
@@ -312,8 +373,8 @@ _3 February 2019_
_23 September 2018_ _23 September 2018_
- Add `CIBW_BUILD` option, for specifying which specific builds to perform (#101) - 🌟 Add `CIBW_BUILD` option, for specifying which specific builds to perform (#101)
- Add support for building Mac and Linux on CircleCI (#91, #97) - 🌟 Add support for building Mac and Linux on CircleCI (#91, #97)
- 🛠 Improved support for building universal wheels (#95) - 🛠 Improved support for building universal wheels (#95)
- 🛠 Ensure log output is unbuffered and therefore in the correct order (#92) - 🛠 Ensure log output is unbuffered and therefore in the correct order (#92)
- 🛠 Improved error reporting for errors that occur inside a package's setup.py (#88) - 🛠 Improved error reporting for errors that occur inside a package's setup.py (#88)
+2 -1
View File
@@ -21,6 +21,7 @@ jobs:
- job: windows_36 - job: windows_36
pool: {vmImage: 'vs2017-win2016'} pool: {vmImage: 'vs2017-win2016'}
timeoutInMinutes: 180
steps: steps:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
inputs: inputs:
@@ -33,6 +34,7 @@ jobs:
- job: windows_38 - job: windows_38
pool: {vmImage: 'vs2017-win2016'} pool: {vmImage: 'vs2017-win2016'}
timeoutInMinutes: 180
steps: steps:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
inputs: inputs:
@@ -42,4 +44,3 @@ jobs:
- bash: | - bash: |
python -m pip install -r requirements-dev.txt pytest-azurepipelines python -m pip install -r requirements-dev.txt pytest-azurepipelines
python ./bin/run_tests.py python ./bin/run_tests.py
+7 -2
View File
@@ -3,13 +3,18 @@
import os import os
import subprocess import subprocess
import sys import sys
from pathlib import Path
if __name__ == '__main__': if __name__ == '__main__':
# move cwd to the project root # move cwd to the project root
os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.chdir(Path(__file__).resolve().parents[1])
# run the unit tests # run the unit tests
subprocess.check_call([sys.executable, '-m', 'pytest', 'unit_test']) unit_test_args = [sys.executable, '-m', 'pytest', 'unit_test']
# run the docker unit tests only on Linux
if sys.platform.startswith('linux'):
unit_test_args += ['--run-docker']
subprocess.check_call(unit_test_args)
# run the integration tests # run the integration tests
subprocess.check_call([sys.executable, '-m', 'pytest', '-x', '--durations', '0', 'test']) subprocess.check_call([sys.executable, '-m', 'pytest', '-x', '--durations', '0', 'test'])
+1 -1
View File
@@ -1 +1 @@
__version__ = '1.4.2' __version__ = '1.5.5'
+12 -9
View File
@@ -4,6 +4,7 @@ import sys
import textwrap import textwrap
import traceback import traceback
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path
from typing import Any, Dict, List, Optional, overload from typing import Any, Dict, List, Optional, overload
@@ -20,6 +21,7 @@ from cibuildwheel.util import (
BuildSelector, BuildSelector,
DependencyConstraints, DependencyConstraints,
Unbuffered, Unbuffered,
resources_dir,
) )
@@ -114,8 +116,8 @@ def main() -> None:
file=sys.stderr) file=sys.stderr)
exit(2) exit(2)
package_dir = args.package_dir package_dir = Path(args.package_dir)
output_dir = args.output_dir output_dir = Path(args.output_dir)
if platform == 'linux': if platform == 'linux':
repair_command_default = 'auditwheel repair -w {dest_dir} {wheel}' repair_command_default = 'auditwheel repair -w {dest_dir} {wheel}'
@@ -126,6 +128,7 @@ def main() -> None:
build_config, skip_config = os.environ.get('CIBW_BUILD', '*'), os.environ.get('CIBW_SKIP', '') build_config, skip_config = os.environ.get('CIBW_BUILD', '*'), os.environ.get('CIBW_SKIP', '')
environment_config = get_option_from_environment('CIBW_ENVIRONMENT', platform=platform, default='') environment_config = get_option_from_environment('CIBW_ENVIRONMENT', platform=platform, default='')
before_all = get_option_from_environment('CIBW_BEFORE_ALL', platform=platform, default='')
before_build = get_option_from_environment('CIBW_BEFORE_BUILD', platform=platform) before_build = get_option_from_environment('CIBW_BEFORE_BUILD', platform=platform)
repair_command = get_option_from_environment('CIBW_REPAIR_WHEEL_COMMAND', platform=platform, default=repair_command_default) repair_command = get_option_from_environment('CIBW_REPAIR_WHEEL_COMMAND', platform=platform, default=repair_command_default)
dependency_versions = get_option_from_environment('CIBW_DEPENDENCY_VERSIONS', platform=platform, default='pinned') dependency_versions = get_option_from_environment('CIBW_DEPENDENCY_VERSIONS', platform=platform, default='pinned')
@@ -149,7 +152,8 @@ def main() -> None:
elif dependency_versions == 'latest': elif dependency_versions == 'latest':
dependency_constraints = None dependency_constraints = None
else: else:
dependency_constraints = DependencyConstraints(dependency_versions) dependency_versions_path = Path(dependency_versions)
dependency_constraints = DependencyConstraints(dependency_versions_path)
if test_extras: if test_extras:
test_extras = f'[{test_extras}]' test_extras = f'[{test_extras}]'
@@ -163,7 +167,7 @@ def main() -> None:
# This needs to be passed on to the docker container in linux.py # This needs to be passed on to the docker container in linux.py
os.environ['CIBUILDWHEEL'] = '1' os.environ['CIBUILDWHEEL'] = '1'
if not any(os.path.exists(os.path.join(package_dir, name)) if not any((package_dir / name).exists()
for name in ["setup.py", "setup.cfg", "pyproject.toml"]): for name in ["setup.py", "setup.cfg", "pyproject.toml"]):
print('cibuildwheel: Could not find setup.py, setup.cfg or pyproject.toml at root of package', file=sys.stderr) print('cibuildwheel: Could not find setup.py, setup.cfg or pyproject.toml at root of package', file=sys.stderr)
exit(2) exit(2)
@@ -174,9 +178,7 @@ def main() -> None:
manylinux_images: Optional[Dict[str, str]] = None manylinux_images: Optional[Dict[str, str]] = None
if platform == 'linux': if platform == 'linux':
pinned_docker_images_file = os.path.join( pinned_docker_images_file = resources_dir / 'pinned_docker_images.cfg'
os.path.dirname(__file__), 'resources', 'pinned_docker_images.cfg'
)
all_pinned_docker_images = ConfigParser() all_pinned_docker_images = ConfigParser()
all_pinned_docker_images.read(pinned_docker_images_file) all_pinned_docker_images.read(pinned_docker_images_file)
# all_pinned_docker_images looks like a dict of dicts, e.g. # all_pinned_docker_images looks like a dict of dicts, e.g.
@@ -211,6 +213,7 @@ def main() -> None:
test_extras=test_extras, test_extras=test_extras,
before_test=before_test, before_test=before_test,
before_build=before_build, before_build=before_build,
before_all=before_all,
build_verbosity=build_verbosity, build_verbosity=build_verbosity,
build_selector=build_selector, build_selector=build_selector,
repair_command=repair_command, repair_command=repair_command,
@@ -224,8 +227,8 @@ def main() -> None:
print_preamble(platform, build_options) print_preamble(platform, build_options)
if not os.path.exists(output_dir): if not output_dir.exists():
os.makedirs(output_dir) output_dir.mkdir(parents=True)
if platform == 'linux': if platform == 'linux':
cibuildwheel.linux.build(build_options) cibuildwheel.linux.build(build_options)
+55 -25
View File
@@ -1,17 +1,23 @@
import shlex
import subprocess import subprocess
from typing import Callable, Dict, List, NamedTuple, Optional, Sequence
from typing import Dict, NamedTuple
import bashlex # type: ignore import bashlex # type: ignore
# a function that takes a command and the environment, and returns the result
EnvironmentExecutor = Callable[[List[str], Dict[str, str]], str]
def local_environment_executor(command: List[str], env: Dict[str, str]) -> str:
return subprocess.check_output(command, env=env, universal_newlines=True)
class NodeExecutionContext(NamedTuple): class NodeExecutionContext(NamedTuple):
environment: Dict[str, str] environment: Dict[str, str]
input: str input: str
executor: EnvironmentExecutor
def evaluate(value: str, environment: Dict[str, str]) -> str: def evaluate(value: str, environment: Dict[str, str], executor: Optional[EnvironmentExecutor] = None) -> str:
if not value: if not value:
# empty string evaluates to empty string # empty string evaluates to empty string
# (but trips up bashlex) # (but trips up bashlex)
@@ -26,7 +32,7 @@ def evaluate(value: str, environment: Dict[str, str]) -> str:
return evaluate_node( return evaluate_node(
value_word_node, value_word_node,
context=NodeExecutionContext(environment=environment, input=value) context=NodeExecutionContext(environment=environment, input=value, executor=executor or local_environment_executor)
) )
@@ -34,40 +40,64 @@ def evaluate_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str:
if node.kind == 'word': if node.kind == 'word':
return evaluate_word_node(node, context=context) return evaluate_word_node(node, context=context)
elif node.kind == 'commandsubstitution': elif node.kind == 'commandsubstitution':
return evaluate_command_node(node.command, context=context) node_result = evaluate_command_node(node.command, context=context)
# bash removes training newlines in command substitution
return node_result.rstrip()
elif node.kind == 'parameter': elif node.kind == 'parameter':
return evaluate_parameter_node(node, context=context) return evaluate_parameter_node(node, context=context)
else: else:
raise ValueError(f'Unsupported bash construct: "{node.word}"') raise ValueError(f'Unsupported bash construct: "{node.kind}"')
def evaluate_word_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str: def evaluate_word_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str:
word_start = node.pos[0] value = node.word
word_end = node.pos[1]
word_string = context.input[word_start:word_end]
letters = list(word_string)
for part in node.parts: for part in node.parts:
part_start = part.pos[0] - word_start part_string = context.input[part.pos[0]:part.pos[1]]
part_end = part.pos[1] - word_start part_value = evaluate_node(part, context=context)
# Set all the characters in the part to None if part_string not in value:
for i in range(part_start, part_end): raise RuntimeError(
letters[i] = '' 'bash parse failed. part "{}" not found in "{}". Word was "{}". Full input was "{}"'.format(
part_string, value, node.word, context.input,
)
)
letters[part_start] = evaluate_node(part, context=context) value = value.replace(part_string, part_value, 1)
# remove the None letters and concat return value
value = ''.join(letters)
# apply bash-like quotes/whitespace treatment
return ' '.join(word.strip() for word in shlex.split(value))
def evaluate_command_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str: def evaluate_command_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str:
words = [evaluate_node(part, context=context) for part in node.parts] if any(n.kind == 'operator' for n in node.parts):
command = ' '.join(words) return evaluate_nodes_as_compound_command(node.parts, context=context)
return subprocess.check_output(shlex.split(command), env=context.environment, universal_newlines=True) else:
return evaluate_nodes_as_simple_command(node.parts, context=context)
def evaluate_nodes_as_compound_command(nodes: Sequence[bashlex.ast.node], context: NodeExecutionContext) -> str:
# bashlex doesn't support any operators besides ';' inside command
# substitutions, so we only need to handle that case. We do so assuming
# that `set -o errexit` is on, because it's easier to code!
result = ''
for node in nodes:
if node.kind == 'command':
result += evaluate_command_node(node, context=context)
elif node.kind == 'operator':
if node.op == ';':
pass
else:
raise ValueError(f'Unsupported bash operator: "{node.op}"')
else:
raise ValueError(f'Unsupported bash node in compound command: "{node.kind}"')
return result
def evaluate_nodes_as_simple_command(nodes: List[bashlex.ast.node], context: NodeExecutionContext):
command = [evaluate_node(part, context=context) for part in nodes]
return context.executor(command, context.environment)
def evaluate_parameter_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str: def evaluate_parameter_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str:
+192
View File
@@ -0,0 +1,192 @@
import io
import json
import os
import shlex
import subprocess
import sys
import uuid
from os import PathLike
from pathlib import Path, PurePath
from typing import IO, Dict, List, Optional, Sequence, Union
class DockerContainer:
'''
An object that represents a running Docker container.
Intended for use as a context manager e.g.
`with DockerContainer('ubuntu') as docker:`
A bash shell is running in the remote container. When `call()` is invoked,
the command is relayed to the remote shell, and the results are streamed
back to cibuildwheel.
'''
UTILITY_PYTHON = '/opt/python/cp38-cp38/bin/python'
process: subprocess.Popen
bash_stdin: IO[bytes]
bash_stdout: IO[bytes]
def __init__(self, docker_image: str, simulate_32_bit: bool = False, cwd: Optional[Union[str, PathLike]] = None):
self.docker_image = docker_image
self.simulate_32_bit = simulate_32_bit
self.cwd = cwd
def __enter__(self) -> 'DockerContainer':
self.name = f'cibuildwheel-{uuid.uuid4()}'
cwd_args = ['-w', str(self.cwd)] if self.cwd else []
shell_args = ['linux32', '/bin/bash'] if self.simulate_32_bit else ['/bin/bash']
subprocess.run(
[
'docker', 'create',
'--env', 'CIBUILDWHEEL',
'--name', self.name,
'-i',
'-v', '/:/host', # ignored on CircleCI
*cwd_args,
self.docker_image,
*shell_args
],
check=True,
)
self.process = subprocess.Popen(
[
'docker', 'start',
'--attach', '--interactive',
self.name,
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
assert self.process.stdin and self.process.stdout
self.bash_stdin = self.process.stdin
self.bash_stdout = self.process.stdout
# run a noop command to block until the container is responding
self.call(['/bin/true'])
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.bash_stdin.close()
self.process.terminate()
self.process.wait()
subprocess.run(['docker', 'rm', '--force', '-v', self.name])
self.name = None
def copy_into(self, from_path: Path, to_path: PurePath) -> None:
# `docker cp` causes 'no space left on device' error when
# a container is running and the host filesystem is
# mounted. https://github.com/moby/moby/issues/38995
# Use `docker exec` instead.
if from_path.is_dir():
self.call(['mkdir', '-p', to_path])
subprocess.run(
f'tar cf - . | docker exec -i {self.name} tar -xC {shell_quote(to_path)} -f -',
shell=True,
check=True,
cwd=from_path)
else:
subprocess.run(
f'cat {shell_quote(from_path)} | docker exec -i {self.name} sh -c "cat > {shell_quote(to_path)}"',
shell=True,
check=True)
def copy_out(self, from_path: PurePath, to_path: Path) -> None:
# note: we assume from_path is a dir
to_path.mkdir(parents=True, exist_ok=True)
subprocess.run(
f'docker exec -i {self.name} tar -cC {shell_quote(from_path)} -f - . | tar -xf -',
shell=True,
check=True,
cwd=to_path
)
def glob(self, path: PurePath, pattern: str) -> List[PurePath]:
glob_pattern = os.path.join(str(path), pattern)
path_strs = json.loads(self.call([
self.UTILITY_PYTHON,
'-c',
f'import sys, json, glob; json.dump(glob.glob({glob_pattern!r}), sys.stdout)'
], capture_output=True))
return [PurePath(p) for p in path_strs]
def call(self, args: Sequence[Union[str, PathLike]], env: Dict[str, str] = {},
capture_output=False, cwd: Optional[Union[str, PathLike]] = None) -> str:
chdir = f'cd {cwd}' if cwd else ''
env_assignments = ' '.join(f'{shlex.quote(k)}={shlex.quote(v)}'
for k, v in env.items())
command = ' '.join(shlex.quote(str(a)) for a in args)
end_of_message = str(uuid.uuid4())
# log the command we're executing
print(f' + {command}')
# Write a command to the remote shell. First we change the
# cwd, if that's required. Then, we use the `env` utility to run
# `command` inside the specified environment. We use `env` because it
# can cope with spaces and strange characters in the name or value.
# Finally, the remote shell is told to write a footer - this will show
# up in the output so we know when to stop reading, and will include
# the returncode of `command`.
self.bash_stdin.write(bytes(f'''(
{chdir}
env {env_assignments} {command}
printf "%04d%s\n" $? {end_of_message}
)
''', encoding='utf8', errors='surrogateescape'))
self.bash_stdin.flush()
if capture_output:
output_io: IO[bytes] = io.BytesIO()
else:
output_io = sys.stdout.buffer
while True:
line = self.bash_stdout.readline()
if line.endswith(b'%s\n' % (bytes(end_of_message, encoding='utf8'))):
footer_offset = (
len(line)
- 1 # newline character
- len(end_of_message) # delimiter
- 4 # 4 returncode decimals
)
returncode_str = line[footer_offset:footer_offset+4]
returncode = int(returncode_str)
# add the last line to output, without the footer
output_io.write(line[0:footer_offset])
break
else:
output_io.write(line)
if isinstance(output_io, io.BytesIO):
output = str(output_io.getvalue(), encoding='utf8', errors='surrogateescape')
else:
output = ''
if returncode != 0:
raise subprocess.CalledProcessError(returncode, args, output)
return output
def get_environment(self) -> Dict[str, str]:
return json.loads(self.call([
self.UTILITY_PYTHON,
'-c',
'import sys, json, os; json.dump(os.environ.copy(), sys.stdout)'
], capture_output=True))
def environment_executor(self, command: List[str], environment: Dict[str, str]) -> str:
# used as an EnvironmentExecutor to evaluate commands and capture output
return self.call(command, env=environment, capture_output=True)
def shell_quote(path: PurePath) -> str:
return shlex.quote(str(path))
+8 -6
View File
@@ -1,6 +1,6 @@
import bashlex # type: ignore import bashlex # type: ignore
from typing import Dict, List from typing import Dict, List, Mapping, Optional
from . import bashlex_eval from . import bashlex_eval
@@ -46,9 +46,9 @@ class EnvironmentAssignment:
self.name = name self.name = name
self.value = value self.value = value
def evaluated_value(self, environment: Dict[str, str]) -> str: def evaluated_value(self, environment: Dict[str, str], executor: Optional[bashlex_eval.EnvironmentExecutor] = None) -> str:
'''Returns the value of this assignment, as evaluated in the environment''' '''Returns the value of this assignment, as evaluated in the environment'''
return bashlex_eval.evaluate(self.value, environment=environment) return bashlex_eval.evaluate(self.value, environment=environment, executor=executor)
def as_shell_assignment(self) -> str: def as_shell_assignment(self) -> str:
return f'export {self.name}={self.value}' return f'export {self.name}={self.value}'
@@ -61,11 +61,13 @@ class ParsedEnvironment:
def __init__(self, assignments: List[EnvironmentAssignment]): def __init__(self, assignments: List[EnvironmentAssignment]):
self.assignments = assignments self.assignments = assignments
def as_dictionary(self, prev_environment: Dict[str, str]) -> Dict[str, str]: def as_dictionary(self,
environment = prev_environment.copy() prev_environment: Mapping[str, str],
executor: Optional[bashlex_eval.EnvironmentExecutor] = None) -> Dict[str, str]:
environment = dict(**prev_environment)
for assignment in self.assignments: for assignment in self.assignments:
value = assignment.evaluated_value(environment=environment) value = assignment.evaluated_value(environment=environment, executor=executor)
environment[assignment.name] = value environment[assignment.name] = value
return environment return environment
+130 -187
View File
@@ -1,26 +1,14 @@
import os
import platform import platform
import shlex
import subprocess import subprocess
import sys import sys
import textwrap import textwrap
import uuid from os import PathLike
from pathlib import Path, PurePath
from typing import List, NamedTuple, Union
from typing import List, NamedTuple, Optional, Union from .docker_container import DockerContainer
from .util import (BuildOptions, BuildSelector,
from .util import ( get_build_verbosity_extra_flags, prepare_command)
BuildOptions,
BuildSelector,
get_build_verbosity_extra_flags,
prepare_command,
)
def call(args: List[str], input: Optional[Union[str, bytes]] = None, universal_newlines: bool = False) -> None:
print('+ ' + ' '.join(shlex.quote(a) for a in args))
subprocess.run(
args, input=input, universal_newlines=universal_newlines, check=True
)
def matches_platform(identifier: str) -> bool: def matches_platform(identifier: str) -> bool:
@@ -47,42 +35,46 @@ def matches_platform(identifier: str) -> bool:
class PythonConfiguration(NamedTuple): class PythonConfiguration(NamedTuple):
version: str version: str
identifier: str identifier: str
path: str path_str: str
@property
def path(self):
return PurePath(self.path_str)
def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfiguration]: def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfiguration]:
python_configurations = [ python_configurations = [
PythonConfiguration(version='2.7', identifier='cp27-manylinux_x86_64', path='/opt/python/cp27-cp27m'), PythonConfiguration(version='2.7', identifier='cp27-manylinux_x86_64', path_str='/opt/python/cp27-cp27m'),
PythonConfiguration(version='2.7', identifier='cp27-manylinux_x86_64', path='/opt/python/cp27-cp27mu'), PythonConfiguration(version='2.7', identifier='cp27-manylinux_x86_64', path_str='/opt/python/cp27-cp27mu'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_x86_64', path='/opt/python/cp35-cp35m'), PythonConfiguration(version='3.5', identifier='cp35-manylinux_x86_64', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_x86_64', path='/opt/python/cp36-cp36m'), PythonConfiguration(version='3.6', identifier='cp36-manylinux_x86_64', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_x86_64', path='/opt/python/cp37-cp37m'), PythonConfiguration(version='3.7', identifier='cp37-manylinux_x86_64', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_x86_64', path='/opt/python/cp38-cp38'), PythonConfiguration(version='3.8', identifier='cp38-manylinux_x86_64', path_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.9', identifier='cp39-manylinux_x86_64', path='/opt/python/cp39-cp39'), PythonConfiguration(version='3.9', identifier='cp39-manylinux_x86_64', path_str='/opt/python/cp39-cp39'),
PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path='/opt/python/cp27-cp27m'), PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path_str='/opt/python/cp27-cp27m'),
PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path='/opt/python/cp27-cp27mu'), PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path_str='/opt/python/cp27-cp27mu'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_i686', path='/opt/python/cp35-cp35m'), PythonConfiguration(version='3.5', identifier='cp35-manylinux_i686', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_i686', path='/opt/python/cp36-cp36m'), PythonConfiguration(version='3.6', identifier='cp36-manylinux_i686', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_i686', path='/opt/python/cp37-cp37m'), PythonConfiguration(version='3.7', identifier='cp37-manylinux_i686', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_i686', path='/opt/python/cp38-cp38'), PythonConfiguration(version='3.8', identifier='cp38-manylinux_i686', path_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.9', identifier='cp39-manylinux_i686', path='/opt/python/cp39-cp39'), PythonConfiguration(version='3.9', identifier='cp39-manylinux_i686', path_str='/opt/python/cp39-cp39'),
PythonConfiguration(version='2.7', identifier='pp27-manylinux_x86_64', path='/opt/python/pp27-pypy_73'), PythonConfiguration(version='2.7', identifier='pp27-manylinux_x86_64', path_str='/opt/python/pp27-pypy_73'),
PythonConfiguration(version='3.6', identifier='pp36-manylinux_x86_64', path='/opt/python/pp36-pypy36_pp73'), PythonConfiguration(version='3.6', identifier='pp36-manylinux_x86_64', path_str='/opt/python/pp36-pypy36_pp73'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_aarch64', path='/opt/python/cp35-cp35m'), PythonConfiguration(version='3.5', identifier='cp35-manylinux_aarch64', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_aarch64', path='/opt/python/cp36-cp36m'), PythonConfiguration(version='3.6', identifier='cp36-manylinux_aarch64', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_aarch64', path='/opt/python/cp37-cp37m'), PythonConfiguration(version='3.7', identifier='cp37-manylinux_aarch64', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_aarch64', path='/opt/python/cp38-cp38'), PythonConfiguration(version='3.8', identifier='cp38-manylinux_aarch64', path_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.9', identifier='cp39-manylinux_aarch64', path='/opt/python/cp39-cp39'), PythonConfiguration(version='3.9', identifier='cp39-manylinux_aarch64', path_str='/opt/python/cp39-cp39'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_ppc64le', path='/opt/python/cp35-cp35m'), PythonConfiguration(version='3.5', identifier='cp35-manylinux_ppc64le', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_ppc64le', path='/opt/python/cp36-cp36m'), PythonConfiguration(version='3.6', identifier='cp36-manylinux_ppc64le', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_ppc64le', path='/opt/python/cp37-cp37m'), PythonConfiguration(version='3.7', identifier='cp37-manylinux_ppc64le', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_ppc64le', path='/opt/python/cp38-cp38'), PythonConfiguration(version='3.8', identifier='cp38-manylinux_ppc64le', path_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.9', identifier='cp39-manylinux_ppc64le', path='/opt/python/cp39-cp39'), PythonConfiguration(version='3.9', identifier='cp39-manylinux_ppc64le', path_str='/opt/python/cp39-cp39'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_s390x', path='/opt/python/cp35-cp35m'), PythonConfiguration(version='3.5', identifier='cp35-manylinux_s390x', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_s390x', path='/opt/python/cp36-cp36m'), PythonConfiguration(version='3.6', identifier='cp36-manylinux_s390x', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_s390x', path='/opt/python/cp37-cp37m'), PythonConfiguration(version='3.7', identifier='cp37-manylinux_s390x', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_s390x', path='/opt/python/cp38-cp38'), PythonConfiguration(version='3.8', identifier='cp38-manylinux_s390x', path_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.9', identifier='cp39-manylinux_s390x', path='/opt/python/cp39-cp39'), PythonConfiguration(version='3.9', identifier='cp39-manylinux_s390x', path_str='/opt/python/cp39-cp39'),
] ]
# skip builds as required # skip builds as required
return [c for c in python_configurations if matches_platform(c.identifier) and build_selector(c.identifier)] return [c for c in python_configurations if matches_platform(c.identifier) and build_selector(c.identifier)]
@@ -109,113 +101,106 @@ def build(options: BuildOptions) -> None:
('pp', 'manylinux_x86_64', options.manylinux_images['pypy_x86_64']), ('pp', 'manylinux_x86_64', options.manylinux_images['pypy_x86_64']),
] ]
if not os.path.realpath(options.package_dir).startswith(os.path.realpath('.')): cwd = Path.cwd()
abs_package_dir = options.package_dir.resolve()
if cwd != abs_package_dir and cwd not in abs_package_dir.parents:
raise Exception('package_dir must be inside the working directory') raise Exception('package_dir must be inside the working directory')
container_package_dir = os.path.join('/project', os.path.relpath(options.package_dir, '.')) container_project_path = PurePath('/project')
container_package_dir = container_project_path / abs_package_dir.relative_to(cwd)
container_output_dir = PurePath('/output')
for implementation, platform_tag, docker_image in platforms: for implementation, platform_tag, docker_image in platforms:
platform_configs = [c for c in python_configurations if c.identifier.startswith(implementation) and c.identifier.endswith(platform_tag)] platform_configs = [c for c in python_configurations if c.identifier.startswith(implementation) and c.identifier.endswith(platform_tag)]
if not platform_configs: if not platform_configs:
continue continue
shell_cmd = ['linux32', '/bin/bash'] if platform_tag.endswith("i686") else ['/bin/bash']
container_name = f'cibuildwheel-{uuid.uuid4()}'
call(['docker', 'create',
'--env', 'CIBUILDWHEEL',
'--name', container_name,
'-i',
'-v', '/:/host', # ignored on CircleCI
docker_image,
'/bin/bash'])
try: try:
call(['docker', 'cp', '.', container_name + ':/project']) with DockerContainer(docker_image, simulate_32_bit=platform_tag.endswith('i686'), cwd=container_project_path) as docker:
docker.copy_into(Path.cwd(), container_project_path)
call(['docker', 'start', container_name]) if options.before_all:
env = docker.get_environment()
env['PATH'] = f'/opt/python/cp38-cp38:{env["PATH"]}'
env = options.environment.as_dictionary(env, executor=docker.environment_executor)
before_all_prepared = prepare_command(options.before_all, project=container_project_path, package=container_package_dir)
docker.call(['sh', '-c', before_all_prepared], env=env)
for config in platform_configs: for config in platform_configs:
dependency_constraint_flags: List[Union[str, PathLike]] = []
if options.dependency_constraints: if options.dependency_constraints:
constraints_file = options.dependency_constraints.get_for_python_version(config.version) constraints_file = options.dependency_constraints.get_for_python_version(config.version)
container_constraints_file = PurePath('/constraints.txt')
# `docker cp` causes 'no space left on device' error when docker.copy_into(constraints_file, container_constraints_file)
# a container is running and the host filesystem is dependency_constraint_flags = ['-c', container_constraints_file]
# mounted. https://github.com/moby/moby/issues/38995
# Use `docker exec` instead.
with open(constraints_file, 'rb') as f:
call(
['docker', 'exec', '-i', container_name, 'sh', '-c', 'cat > /constraints.txt'],
input=f.read(),
)
call( env = docker.get_environment()
['docker', 'exec', '-i', container_name] + shell_cmd,
universal_newlines=True,
input='''
# give xtrace output an extra level of indent inside docker
PS4=' + '
set -o errexit # put this config's python top of the list
set -o xtrace python_bin = config.path / 'bin'
mkdir -p /output env['PATH'] = f'{python_bin}:{env["PATH"]}'
cd /project
PYBIN="{config_python_bin}" env = options.environment.as_dictionary(env, executor=docker.environment_executor)
export PATH="$PYBIN:$PATH" # check config python and pip are still on PATH
{environment_exports} which_python = docker.call(['which', 'python'], env=env, capture_output=True).strip()
if PurePath(which_python) != python_bin / 'python':
print("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.", file=sys.stderr)
exit(1)
# check the active python and pip are in PYBIN which_pip = docker.call(['which', 'pip'], env=env, capture_output=True).strip()
if [ "$(which pip)" != "$PYBIN/pip" ]; then if PurePath(which_pip) != python_bin / 'pip':
echo "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." print("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.", file=sys.stderr)
exit 1 exit(1)
fi
if [ "$(which python)" != "$PYBIN/python" ]; then
echo "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."
exit 1
fi
if [ ! -z {before_build} ]; then if options.before_build:
sh -c {before_build} before_build_prepared = prepare_command(options.before_build, project=container_project_path, package=container_package_dir)
fi docker.call(['sh', '-c', before_build_prepared], env=env)
# Build the wheel temp_dir = PurePath('/tmp/cibuildwheel')
rm -rf /tmp/built_wheel built_wheel_dir = temp_dir / 'built_wheel'
mkdir /tmp/built_wheel docker.call(['rm', '-rf', built_wheel_dir])
pip wheel {package_dir} -w /tmp/built_wheel --no-deps {build_verbosity_flag} docker.call(['mkdir', '-p', built_wheel_dir])
built_wheel=(/tmp/built_wheel/*.whl)
# repair the wheel docker.call([
rm -rf /tmp/repaired_wheels 'pip', 'wheel',
mkdir /tmp/repaired_wheels container_package_dir,
# NOTE: 'built_wheel' here is a bash array of glob matches; "$built_wheel" returns '-w', built_wheel_dir,
# the first element '--no-deps',
if [[ "$built_wheel" == *none-any.whl ]] || [ -z {repair_command} ]; then *get_build_verbosity_extra_flags(options.build_verbosity)
# pure Python wheel or empty repair command ], env=env)
mv "$built_wheel" /tmp/repaired_wheels
else
sh -c {repair_command} repair_command "$built_wheel"
fi
repaired_wheels=(/tmp/repaired_wheels/*.whl)
if [ ! -z {test_command} ]; then built_wheel = docker.glob(built_wheel_dir, '*.whl')[0]
# Set up a virtual environment to install and test from, to make sure
repaired_wheel_dir = temp_dir / 'repaired_wheel'
docker.call(['rm', '-rf', repaired_wheel_dir])
docker.call(['mkdir', '-p', repaired_wheel_dir])
if built_wheel.name.endswith('none-any.whl') or not options.repair_command:
docker.call(['mv', built_wheel, repaired_wheel_dir])
else:
repair_command_prepared = prepare_command(options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir)
docker.call(['sh', '-c', repair_command_prepared], env=env)
repaired_wheels = docker.glob(repaired_wheel_dir, '*.whl')
if options.test_command:
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time. # there are no dependencies that were pulled in at build time.
pip install {dependency_install_flags} virtualenv docker.call(['pip', 'install', 'virtualenv', *dependency_constraint_flags], env=env)
venv_dir=`mktemp -d`/venv venv_dir = PurePath(docker.call(['mktemp', '-d'], capture_output=True).strip()) / 'venv'
python -m virtualenv --no-download "$venv_dir"
# run the tests in a subshell to keep that `activate` docker.call(['python', '-m', 'virtualenv', '--no-download', venv_dir], env=env)
# script from polluting the env
(
source "$venv_dir/bin/activate"
echo "Running tests using `which python`" virtualenv_env = env.copy()
virtualenv_env['PATH'] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}"
if [ ! -z {before_test} ]; then if options.before_test:
sh -c {before_test} before_test_prepared = prepare_command(options.before_test, project=container_project_path, package=container_package_dir)
fi docker.call(['sh', '-c', before_test_prepared], env=virtualenv_env)
# Install the wheel we just built # Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel # Note: If auditwheel produced two wheels, it's because the earlier produced wheel
@@ -223,79 +208,37 @@ def build(options: BuildOptions) -> None:
# functionally the same, differing only in name, wheel metadata, and possibly include # 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. # different external shared libraries. so it doesn't matter which one we run the tests on.
# Let's just pick the first one. # Let's just pick the first one.
pip install "${{repaired_wheels[0]}}"{test_extras} wheel_to_test = repaired_wheels[0]
docker.call(['pip', 'install', str(wheel_to_test) + options.test_extras], env=virtualenv_env)
# Install any requirements to run the tests # Install any requirements to run the tests
if [ ! -z "{test_requires}" ]; then if options.test_requires:
pip install {test_requires} docker.call(['pip', 'install', *options.test_requires], env=virtualenv_env)
fi
# Run the tests from a different directory # Run the tests from a different directory
pushd $HOME test_command_prepared = prepare_command(options.test_command, project=container_project_path, package=container_package_dir)
sh -c {test_command} docker.call(['sh', '-c', test_command_prepared], cwd='/root', env=virtualenv_env)
popd
)
# exit if tests failed (needed for older bash versions)
if [ $? -ne 0 ]; then
exit 1;
fi
# clean up # clean up test environment
rm -rf "$venv_dir" docker.call(['rm', '-rf', venv_dir])
fi
# we're all done here; move it to output # move repaired wheels to output
mv "${{repaired_wheels[@]}}" /output docker.call(['mkdir', '-p', container_output_dir])
for repaired_wheel in "${{repaired_wheels[@]}}"; do docker.call(['mv', *repaired_wheels, container_output_dir])
chown {uid}:{gid} "/output/$(basename "$repaired_wheel")"
done
'''.format(
config_python_bin=config.path + '/bin',
package_dir=container_package_dir,
test_requires=' '.join(options.test_requires),
test_extras=options.test_extras,
test_command=shlex.quote(
prepare_command(options.test_command, project='/project', package=container_package_dir) if options.test_command else ''
),
before_build=shlex.quote(
prepare_command(options.before_build, project='/project', package=container_package_dir) if options.before_build else ''
),
build_verbosity_flag=' '.join(get_build_verbosity_extra_flags(options.build_verbosity)),
repair_command=shlex.quote(
prepare_command(options.repair_command, wheel='"$1"', dest_dir='/tmp/repaired_wheels') if options.repair_command else ''
),
environment_exports='\n'.join(options.environment.as_shell_commands()),
uid=os.getuid(),
gid=os.getgid(),
before_test=shlex.quote(
prepare_command(options.before_test, project='/project', package=container_package_dir) if options.before_test else ''
),
dependency_install_flags='-c /constraints.txt' if options.dependency_constraints else '',
)
)
# copy the output back into the host # copy the output back into the host
call(['docker', 'cp', docker.copy_out(container_output_dir, options.output_dir)
container_name + ':/output/.',
os.path.abspath(options.output_dir)])
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
print(f'Command {error.cmd} failed with code {error.returncode}. {error.stdout}')
troubleshoot(options.package_dir, error) troubleshoot(options.package_dir, error)
exit(1) exit(1)
finally:
# Still gets executed, even when 'exit(1)' gets called
call(['docker', 'rm', '--force', '-v', container_name])
def troubleshoot(package_dir: str, error: Exception) -> None: def troubleshoot(package_dir: Path, error: Exception) -> None:
if (isinstance(error, subprocess.CalledProcessError) and 'exec' in error.cmd): if (isinstance(error, subprocess.CalledProcessError) and 'exec' in error.cmd):
# the bash script failed # the bash script failed
print('Checking for common errors...') print('Checking for common errors...')
so_files = [] so_files = list(package_dir.glob('**/*.so'))
for root, dirs, files in os.walk(package_dir):
for name in files:
_, ext = os.path.splitext(name)
if ext == '.so':
so_files.append(os.path.join(root, name))
if so_files: if so_files:
print(textwrap.dedent(''' print(textwrap.dedent('''
@@ -309,5 +252,5 @@ def troubleshoot(package_dir: str, error: Exception) -> None:
''')) '''))
print(' Files detected:') print(' Files detected:')
print('\n'.join([' ' + f for f in so_files])) print('\n'.join([f' {f}' for f in so_files]))
print('') print('')
+74 -63
View File
@@ -4,27 +4,22 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from glob import glob from os import PathLike
from pathlib import Path
from typing import Dict, List, Optional, NamedTuple, Union from typing import Dict, List, NamedTuple, Optional, Sequence, Union
from .environment import ParsedEnvironment from .environment import ParsedEnvironment
from .util import ( from .util import (BuildOptions, BuildSelector, download,
BuildOptions, get_build_verbosity_extra_flags, get_pip_script,
BuildSelector, prepare_command)
download,
get_build_verbosity_extra_flags,
get_pip_script,
prepare_command,
)
def call(args: Union[str, List[str]], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None, shell: bool = False) -> int: def call(args: Union[str, Sequence[Union[str, PathLike]]], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None, shell: bool = False) -> int:
# print the command executing for the logs # print the command executing for the logs
if shell: if shell:
print(f'+ {args}') print(f'+ {args}')
else: else:
print('+ ' + ' '.join(shlex.quote(a) for a in args)) print('+ ' + ' '.join(shlex.quote(str(a)) for a in args))
return subprocess.check_call(args, env=env, cwd=cwd, shell=shell) return subprocess.check_call(args, env=env, cwd=cwd, shell=shell)
@@ -41,8 +36,8 @@ def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfi
PythonConfiguration(version='2.7', identifier='cp27-macosx_x86_64', url='https://www.python.org/ftp/python/2.7.18/python-2.7.18-macosx10.9.pkg'), PythonConfiguration(version='2.7', identifier='cp27-macosx_x86_64', url='https://www.python.org/ftp/python/2.7.18/python-2.7.18-macosx10.9.pkg'),
PythonConfiguration(version='3.5', identifier='cp35-macosx_x86_64', url='https://www.python.org/ftp/python/3.5.4/python-3.5.4-macosx10.6.pkg'), PythonConfiguration(version='3.5', identifier='cp35-macosx_x86_64', url='https://www.python.org/ftp/python/3.5.4/python-3.5.4-macosx10.6.pkg'),
PythonConfiguration(version='3.6', identifier='cp36-macosx_x86_64', url='https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg'), PythonConfiguration(version='3.6', identifier='cp36-macosx_x86_64', url='https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg'),
PythonConfiguration(version='3.7', identifier='cp37-macosx_x86_64', url='https://www.python.org/ftp/python/3.7.7/python-3.7.7-macosx10.9.pkg'), PythonConfiguration(version='3.7', identifier='cp37-macosx_x86_64', url='https://www.python.org/ftp/python/3.7.8/python-3.7.8-macosx10.9.pkg'),
PythonConfiguration(version='3.8', identifier='cp38-macosx_x86_64', url='https://www.python.org/ftp/python/3.8.3/python-3.8.3-macosx10.9.pkg'), PythonConfiguration(version='3.8', identifier='cp38-macosx_x86_64', url='https://www.python.org/ftp/python/3.8.4/python-3.8.4-macosx10.9.pkg'),
PythonConfiguration(version='3.9', identifier='cp39-macosx_x86_64', url='https://www.python.org/ftp/python/3.9.0/python-3.9.0b3-macosx10.9.pkg'), PythonConfiguration(version='3.9', identifier='cp39-macosx_x86_64', url='https://www.python.org/ftp/python/3.9.0/python-3.9.0b3-macosx10.9.pkg'),
# PyPy # PyPy
PythonConfiguration(version='2.7', identifier='pp27-macosx_x86_64', url='https://downloads.python.org/pypy/pypy2.7-v7.3.1-osx64.tar.bz2'), PythonConfiguration(version='2.7', identifier='pp27-macosx_x86_64', url='https://downloads.python.org/pypy/pypy2.7-v7.3.1-osx64.tar.bz2'),
@@ -53,41 +48,41 @@ def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfi
return [c for c in python_configurations if build_selector(c.identifier)] return [c for c in python_configurations if build_selector(c.identifier)]
SYMLINKS_DIR = '/tmp/cibw_bin' SYMLINKS_DIR = Path('/tmp/cibw_bin')
def make_symlinks(installation_bin_path: str, python_executable: str, pip_executable: str) -> None: def make_symlinks(installation_bin_path: Path, python_executable: str, pip_executable: str) -> None:
assert os.path.exists(os.path.join(installation_bin_path, python_executable)) assert (installation_bin_path / python_executable).exists()
# Python bin folders on Mac don't symlink `python3` to `python`, and neither # Python bin folders on Mac don't symlink `python3` to `python`, and neither
# does PyPy for `pypy` or `pypy3`, so we do that so `python` and `pip` always # does PyPy for `pypy` or `pypy3`, so we do that so `python` and `pip` always
# point to the active configuration. # point to the active configuration.
if os.path.exists(SYMLINKS_DIR): if SYMLINKS_DIR.exists():
shutil.rmtree(SYMLINKS_DIR) shutil.rmtree(SYMLINKS_DIR)
os.makedirs(SYMLINKS_DIR) SYMLINKS_DIR.mkdir(parents=True)
os.symlink(os.path.join(installation_bin_path, python_executable), os.path.join(SYMLINKS_DIR, 'python')) (SYMLINKS_DIR / 'python').symlink_to(installation_bin_path / python_executable)
os.symlink(os.path.join(installation_bin_path, python_executable + '-config'), os.path.join(SYMLINKS_DIR, 'python-config')) (SYMLINKS_DIR / 'python-config').symlink_to(installation_bin_path / (python_executable + '-config'))
os.symlink(os.path.join(installation_bin_path, pip_executable), os.path.join(SYMLINKS_DIR, 'pip')) (SYMLINKS_DIR / 'pip').symlink_to(installation_bin_path / pip_executable)
def install_cpython(version: str, url: str) -> str: def install_cpython(version: str, url: str) -> Path:
installed_system_packages = subprocess.check_output(['pkgutil', '--pkgs'], universal_newlines=True).splitlines() installed_system_packages = subprocess.check_output(['pkgutil', '--pkgs'], universal_newlines=True).splitlines()
# if this version of python isn't installed, get it from python.org and install # if this version of python isn't installed, get it from python.org and install
python_package_identifier = f'org.python.Python.PythonFramework-{version}' python_package_identifier = f'org.python.Python.PythonFramework-{version}'
if python_package_identifier not in installed_system_packages: if python_package_identifier not in installed_system_packages:
# download the pkg # download the pkg
download(url, '/tmp/Python.pkg') download(url, Path('/tmp/Python.pkg'))
# install # install
call(['sudo', 'installer', '-pkg', '/tmp/Python.pkg', '-target', '/']) call(['sudo', 'installer', '-pkg', '/tmp/Python.pkg', '-target', '/'])
# patch open ssl # patch open ssl
if version == '3.5': if version == '3.5':
open_ssl_patch_url = f'https://github.com/mayeut/patch-macos-python-openssl/releases/download/v1.0.2u/patch-macos-python-{version}-openssl-v1.0.2u.tar.gz' open_ssl_patch_url = f'https://github.com/mayeut/patch-macos-python-openssl/releases/download/v1.0.2u/patch-macos-python-{version}-openssl-v1.0.2u.tar.gz'
download(open_ssl_patch_url, '/tmp/python-patch.tar.gz') download(open_ssl_patch_url, Path('/tmp/python-patch.tar.gz'))
call(['sudo', 'tar', '-C', f'/Library/Frameworks/Python.framework/Versions/{version}/', '-xmf', '/tmp/python-patch.tar.gz']) call(['sudo', 'tar', '-C', f'/Library/Frameworks/Python.framework/Versions/{version}/', '-xmf', '/tmp/python-patch.tar.gz'])
installation_bin_path = f'/Library/Frameworks/Python.framework/Versions/{version}/bin' installation_bin_path = Path(f'/Library/Frameworks/Python.framework/Versions/{version}/bin')
python_executable = 'python3' if version[0] == '3' else 'python' python_executable = 'python3' if version[0] == '3' else 'python'
pip_executable = 'pip3' if version[0] == '3' else 'pip' pip_executable = 'pip3' if version[0] == '3' else 'pip'
make_symlinks(installation_bin_path, python_executable, pip_executable) make_symlinks(installation_bin_path, python_executable, pip_executable)
@@ -95,16 +90,18 @@ def install_cpython(version: str, url: str) -> str:
return installation_bin_path return installation_bin_path
def install_pypy(version: str, url: str) -> str: def install_pypy(version: str, url: str) -> Path:
pypy_tar_bz2 = url.rsplit('/', 1)[-1] pypy_tar_bz2 = url.rsplit('/', 1)[-1]
assert pypy_tar_bz2.endswith(".tar.bz2") extension = ".tar.bz2"
pypy_base_filename = os.path.splitext(os.path.splitext(pypy_tar_bz2)[0])[0] assert pypy_tar_bz2.endswith(extension)
installation_path = os.path.join('/tmp', pypy_base_filename) pypy_base_filename = pypy_tar_bz2[:-len(extension)]
if not os.path.exists(installation_path): installation_path = Path('/tmp') / pypy_base_filename
download(url, os.path.join("/tmp", pypy_tar_bz2)) if not installation_path.exists():
call(['tar', '-C', '/tmp', '-xf', os.path.join("/tmp", pypy_tar_bz2)]) downloaded_tar_bz2 = Path("/tmp") / pypy_tar_bz2
download(url, downloaded_tar_bz2)
call(['tar', '-C', '/tmp', '-xf', downloaded_tar_bz2])
installation_bin_path = os.path.join(installation_path, 'bin') installation_bin_path = installation_path / 'bin'
python_executable = 'pypy3' if version[0] == '3' else 'pypy' python_executable = 'pypy3' if version[0] == '3' else 'pypy'
pip_executable = 'pip3' if version[0] == '3' else 'pip' pip_executable = 'pip3' if version[0] == '3' else 'pip'
make_symlinks(installation_bin_path, python_executable, pip_executable) make_symlinks(installation_bin_path, python_executable, pip_executable)
@@ -112,7 +109,9 @@ def install_pypy(version: str, url: str) -> str:
return installation_bin_path return installation_bin_path
def setup_python(python_configuration: PythonConfiguration, dependency_constraint_flags: List[str], environment: ParsedEnvironment) -> Dict[str, str]: def setup_python(python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[Union[str, PathLike]],
environment: ParsedEnvironment) -> Dict[str, str]:
if python_configuration.identifier.startswith('cp'): if python_configuration.identifier.startswith('cp'):
installation_bin_path = install_cpython(python_configuration.version, python_configuration.url) installation_bin_path = install_cpython(python_configuration.version, python_configuration.url)
elif python_configuration.identifier.startswith('pp'): elif python_configuration.identifier.startswith('pp'):
@@ -122,8 +121,8 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
env = os.environ.copy() env = os.environ.copy()
env['PATH'] = os.pathsep.join([ env['PATH'] = os.pathsep.join([
SYMLINKS_DIR, str(SYMLINKS_DIR),
installation_bin_path, str(installation_bin_path),
env['PATH'], env['PATH'],
]) ])
@@ -145,15 +144,15 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
exit(1) exit(1)
# install pip & wheel # install pip & wheel
call(['python', get_pip_script] + dependency_constraint_flags, env=env, cwd="/tmp") call(['python', get_pip_script, *dependency_constraint_flags], env=env, cwd="/tmp")
assert os.path.exists(os.path.join(installation_bin_path, 'pip')) assert (installation_bin_path / 'pip').exists()
call(['which', 'pip'], env=env) call(['which', 'pip'], env=env)
call(['pip', '--version'], env=env) call(['pip', '--version'], env=env)
which_pip = subprocess.check_output(['which', 'pip'], env=env, universal_newlines=True).strip() which_pip = subprocess.check_output(['which', 'pip'], env=env, universal_newlines=True).strip()
if which_pip != '/tmp/cibw_bin/pip': if which_pip != '/tmp/cibw_bin/pip':
print("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.", file=sys.stderr) print("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.", file=sys.stderr)
exit(1) exit(1)
call(['pip', 'install', '--upgrade', 'setuptools', 'wheel', 'delocate'] + dependency_constraint_flags, env=env) call(['pip', 'install', '--upgrade', 'setuptools', 'wheel', 'delocate', *dependency_constraint_flags], env=env)
# Set MACOSX_DEPLOYMENT_TARGET to 10.9, if the user didn't set it. # Set MACOSX_DEPLOYMENT_TARGET to 10.9, if the user didn't set it.
# CPython 3.5 defaults to 10.6, and pypy defaults to 10.7, causing # CPython 3.5 defaults to 10.6, and pypy defaults to 10.7, causing
@@ -171,14 +170,19 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
def build(options: BuildOptions) -> None: def build(options: BuildOptions) -> None:
temp_dir = tempfile.mkdtemp(prefix='cibuildwheel') temp_dir = Path(tempfile.mkdtemp(prefix='cibuildwheel'))
built_wheel_dir = os.path.join(temp_dir, 'built_wheel') built_wheel_dir = temp_dir / 'built_wheel'
repaired_wheel_dir = os.path.join(temp_dir, 'repaired_wheel') repaired_wheel_dir = temp_dir / 'repaired_wheel'
if options.before_all:
env = options.environment.as_dictionary(prev_environment=os.environ)
before_all_prepared = prepare_command(options.before_all, project='.', package=options.package_dir)
call([before_all_prepared], shell=True, env=env)
python_configurations = get_python_configurations(options.build_selector) python_configurations = get_python_configurations(options.build_selector)
for config in python_configurations: for config in python_configurations:
dependency_constraint_flags = [] dependency_constraint_flags: Sequence[Union[str, PathLike]] = []
if options.dependency_constraints: if options.dependency_constraints:
dependency_constraint_flags = [ dependency_constraint_flags = [
'-c', options.dependency_constraints.get_for_python_version(config.version) '-c', options.dependency_constraints.get_for_python_version(config.version)
@@ -192,31 +196,39 @@ def build(options: BuildOptions) -> None:
call(before_build_prepared, env=env, shell=True) call(before_build_prepared, env=env, shell=True)
# build the wheel # build the wheel
if os.path.exists(built_wheel_dir): if built_wheel_dir.exists():
shutil.rmtree(built_wheel_dir) shutil.rmtree(built_wheel_dir)
os.makedirs(built_wheel_dir) built_wheel_dir.mkdir(parents=True)
# os.path.abspath is need. Without it pip wheel may try to fetch package from pypi.org
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/joerick/cibuildwheel/pull/369 # see https://github.com/joerick/cibuildwheel/pull/369
call(['pip', 'wheel', os.path.abspath(options.package_dir), '-w', built_wheel_dir, '--no-deps'] + get_build_verbosity_extra_flags(options.build_verbosity), env=env) call([
built_wheel = glob(os.path.join(built_wheel_dir, '*.whl'))[0] 'pip', 'wheel',
options.package_dir.resolve(),
'-w', built_wheel_dir,
'--no-deps',
*get_build_verbosity_extra_flags(options.build_verbosity)
], env=env)
built_wheel = next(built_wheel_dir.glob('*.whl'))
# repair the wheel # repair the wheel
if os.path.exists(repaired_wheel_dir): if repaired_wheel_dir.exists():
shutil.rmtree(repaired_wheel_dir) shutil.rmtree(repaired_wheel_dir)
os.makedirs(repaired_wheel_dir) repaired_wheel_dir.mkdir(parents=True)
if built_wheel.endswith('none-any.whl') or not options.repair_command: if built_wheel.name.endswith('none-any.whl') or not options.repair_command:
# pure Python wheel or empty repair command # pure Python wheel or empty repair command
shutil.move(built_wheel, repaired_wheel_dir) shutil.move(str(built_wheel), repaired_wheel_dir)
else: else:
repair_command_prepared = prepare_command(options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir) repair_command_prepared = prepare_command(options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir)
call(repair_command_prepared, env=env, shell=True) call(repair_command_prepared, env=env, shell=True)
repaired_wheel = glob(os.path.join(repaired_wheel_dir, '*.whl'))[0] repaired_wheel = next(repaired_wheel_dir.glob('*.whl'))
if options.test_command: if options.test_command:
# set up a virtual environment to install and test from, to make sure # set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time. # there are no dependencies that were pulled in at build time.
call(['pip', 'install', 'virtualenv'] + dependency_constraint_flags, env=env) call(['pip', 'install', 'virtualenv', *dependency_constraint_flags], env=env)
venv_dir = tempfile.mkdtemp() venv_dir = Path(tempfile.mkdtemp())
# Use --no-download to ensure determinism by using seed libraries # Use --no-download to ensure determinism by using seed libraries
# built into virtualenv # built into virtualenv
@@ -224,7 +236,7 @@ def build(options: BuildOptions) -> None:
virtualenv_env = env.copy() virtualenv_env = env.copy()
virtualenv_env['PATH'] = os.pathsep.join([ virtualenv_env['PATH'] = os.pathsep.join([
os.path.join(venv_dir, 'bin'), str(venv_dir / 'bin'),
virtualenv_env['PATH'], virtualenv_env['PATH'],
]) ])
@@ -236,7 +248,7 @@ def build(options: BuildOptions) -> None:
call(before_test_prepared, env=virtualenv_env, shell=True) call(before_test_prepared, env=virtualenv_env, shell=True)
# install the wheel # install the wheel
call(['pip', 'install', repaired_wheel + options.test_extras], env=virtualenv_env) call(['pip', 'install', str(repaired_wheel) + options.test_extras], env=virtualenv_env)
# test the wheel # test the wheel
if options.test_requires: if options.test_requires:
@@ -247,8 +259,8 @@ def build(options: BuildOptions) -> None:
# and not the repo code) # and not the repo code)
test_command_prepared = prepare_command( test_command_prepared = prepare_command(
options.test_command, options.test_command,
project=os.path.abspath('.'), project=Path('.').resolve(),
package=os.path.abspath(options.package_dir) package=options.package_dir.resolve()
) )
call(test_command_prepared, cwd=os.environ['HOME'], env=virtualenv_env, shell=True) call(test_command_prepared, cwd=os.environ['HOME'], env=virtualenv_env, shell=True)
@@ -256,5 +268,4 @@ def build(options: BuildOptions) -> None:
shutil.rmtree(venv_dir) shutil.rmtree(venv_dir)
# we're all done here; move it to output (overwrite existing) # we're all done here; move it to output (overwrite existing)
dst = os.path.join(options.output_dir, os.path.basename(repaired_wheel)) shutil.move(str(repaired_wheel), options.output_dir)
shutil.move(repaired_wheel, dst)
+23 -22
View File
@@ -1,14 +1,15 @@
import os import os
import urllib.request import urllib.request
from fnmatch import fnmatch from fnmatch import fnmatch
from pathlib import Path
from time import sleep from time import sleep
from typing import Dict, List, NamedTuple, Optional from typing import Dict, List, NamedTuple, Optional, Union
from .environment import ParsedEnvironment from .environment import ParsedEnvironment
def prepare_command(command: str, **kwargs: str) -> str: def prepare_command(command: str, **kwargs: Union[str, os.PathLike]) -> str:
''' '''
Preprocesses a command by expanding variables like {python}. Preprocesses a command by expanding variables like {python}.
@@ -58,11 +59,11 @@ class Unbuffered:
return getattr(self.stream, attr) return getattr(self.stream, attr)
def download(url: str, dest: str) -> None: def download(url: str, dest: Path) -> None:
print('+ Download ' + url + ' to ' + dest) print(f'+ Download {url} to {dest}')
dest_dir = os.path.dirname(dest) dest_dir = dest.parent
if not os.path.exists(dest_dir): if not dest_dir.exists():
os.makedirs(dest_dir) dest_dir.mkdir(parents=True)
repeat_num = 3 repeat_num = 3
for i in range(repeat_num): for i in range(repeat_num):
@@ -76,42 +77,42 @@ def download(url: str, dest: str) -> None:
break break
try: try:
with open(dest, 'wb') as file: dest.write_bytes(response.read())
file.write(response.read())
finally: finally:
response.close() response.close()
class DependencyConstraints: class DependencyConstraints:
def __init__(self, base_file_path: str): def __init__(self, base_file_path: Path):
assert os.path.exists(base_file_path) assert base_file_path.exists()
self.base_file_path = os.path.abspath(base_file_path) self.base_file_path = base_file_path.resolve()
@staticmethod @staticmethod
def with_defaults() -> 'DependencyConstraints': def with_defaults() -> 'DependencyConstraints':
return DependencyConstraints( return DependencyConstraints(
base_file_path=os.path.join(os.path.dirname(__file__), 'resources', 'constraints.txt') base_file_path=resources_dir / 'constraints.txt'
) )
def get_for_python_version(self, version: str) -> str: def get_for_python_version(self, version: str) -> Path:
version_parts = version.split('.') version_parts = version.split('.')
# try to find a version-specific dependency file e.g. if # try to find a version-specific dependency file e.g. if
# ./constraints.txt is the base, look for ./constraints-python27.txt # ./constraints.txt is the base, look for ./constraints-python27.txt
base, ext = os.path.splitext(self.base_file_path) specific_stem = self.base_file_path.stem + f'-python{version_parts[0]}{version_parts[1]}'
specific = base + f'-python{version_parts[0]}{version_parts[1]}' specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = specific + ext specific_file_path = self.base_file_path.with_name(specific_name)
if os.path.exists(specific_file_path): if specific_file_path.exists():
return specific_file_path return specific_file_path
else: else:
return self.base_file_path return self.base_file_path
class BuildOptions(NamedTuple): class BuildOptions(NamedTuple):
package_dir: str package_dir: Path
output_dir: str output_dir: Path
build_selector: BuildSelector build_selector: BuildSelector
environment: ParsedEnvironment environment: ParsedEnvironment
before_all: str
before_build: Optional[str] before_build: Optional[str]
repair_command: str repair_command: str
manylinux_images: Optional[Dict[str, str]] manylinux_images: Optional[Dict[str, str]]
@@ -123,5 +124,5 @@ class BuildOptions(NamedTuple):
build_verbosity: int build_verbosity: int
resources_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'resources')) resources_dir = Path(__file__).resolve().parent / 'resources'
get_pip_script = os.path.join(resources_dir, 'get-pip.py') get_pip_script = resources_dir / 'get-pip.py'
+123 -79
View File
@@ -3,29 +3,32 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from glob import glob from os import PathLike
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Sequence, Union
from zipfile import ZipFile from zipfile import ZipFile
import toml
from typing import Dict, List, Optional, NamedTuple
from .environment import ParsedEnvironment from .environment import ParsedEnvironment
from .util import ( from .util import (BuildOptions, BuildSelector, download,
BuildOptions, get_build_verbosity_extra_flags, get_pip_script,
BuildSelector, prepare_command)
download,
get_build_verbosity_extra_flags,
get_pip_script,
prepare_command,
)
IS_RUNNING_ON_AZURE = Path('C:\\hostedtoolcache').exists()
IS_RUNNING_ON_AZURE = os.path.exists('C:\\hostedtoolcache')
IS_RUNNING_ON_TRAVIS = os.environ.get('TRAVIS_OS_NAME') == 'windows' IS_RUNNING_ON_TRAVIS = os.environ.get('TRAVIS_OS_NAME') == 'windows'
def shell(args: List[str], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None) -> int: def call(args: Sequence[Union[str, PathLike]], env: Optional[Dict[str, str]] = None,
print('+ ' + ' '.join(args)) cwd: Optional[str] = None) -> None:
return subprocess.check_call(' '.join(args), env=env, cwd=cwd, shell=True) print('+ ' + ' '.join(str(a) for a in args))
# we use shell=True here, even though we don't need a shell due to a bug
# https://bugs.python.org/issue8557
subprocess.check_call([str(a) for a in args], env=env, cwd=cwd, shell=True)
def shell(command: str, env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None) -> None:
print(f'+ {command}')
subprocess.check_call(command, env=env, cwd=cwd, shell=True)
def get_nuget_args(version: str, arch: str) -> List[str]: def get_nuget_args(version: str, arch: str) -> List[str]:
@@ -51,10 +54,10 @@ def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfi
PythonConfiguration(version='3.5.4', arch='64', identifier='cp35-win_amd64', url=None), PythonConfiguration(version='3.5.4', arch='64', identifier='cp35-win_amd64', url=None),
PythonConfiguration(version='3.6.8', arch='32', identifier='cp36-win32', url=None), PythonConfiguration(version='3.6.8', arch='32', identifier='cp36-win32', url=None),
PythonConfiguration(version='3.6.8', arch='64', identifier='cp36-win_amd64', url=None), PythonConfiguration(version='3.6.8', arch='64', identifier='cp36-win_amd64', url=None),
PythonConfiguration(version='3.7.7', arch='32', identifier='cp37-win32', url=None), PythonConfiguration(version='3.7.8', arch='32', identifier='cp37-win32', url=None),
PythonConfiguration(version='3.7.7', arch='64', identifier='cp37-win_amd64', url=None), PythonConfiguration(version='3.7.8', arch='64', identifier='cp37-win_amd64', url=None),
PythonConfiguration(version='3.8.3', arch='32', identifier='cp38-win32', url=None), PythonConfiguration(version='3.8.4', arch='32', identifier='cp38-win32', url=None),
PythonConfiguration(version='3.8.3', arch='64', identifier='cp38-win_amd64', url=None), PythonConfiguration(version='3.8.4', arch='64', identifier='cp38-win_amd64', url=None),
PythonConfiguration(version='3.9.0-b3', arch='32', identifier='cp39-win32', url=None), PythonConfiguration(version='3.9.0-b3', arch='32', identifier='cp39-win32', url=None),
PythonConfiguration(version='3.9.0-b3', arch='64', identifier='cp39-win_amd64', url=None), PythonConfiguration(version='3.9.0-b3', arch='64', identifier='cp39-win_amd64', url=None),
# PyPy # PyPy
@@ -73,36 +76,38 @@ def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfi
return python_configurations return python_configurations
def extract_zip(zip_src: str, dest: str) -> None: def extract_zip(zip_src: Path, dest: Path) -> None:
with ZipFile(zip_src) as zip: with ZipFile(zip_src) as zip:
zip.extractall(dest) zip.extractall(dest)
def install_cpython(version: str, arch: str, nuget: str) -> str: def install_cpython(version: str, arch: str, nuget: Path) -> Path:
nuget_args = get_nuget_args(version, arch) nuget_args = get_nuget_args(version, arch)
installation_path = os.path.join(nuget_args[-1], nuget_args[0] + '.' + version, 'tools') installation_path = Path(nuget_args[-1]) / (nuget_args[0] + '.' + version) / 'tools'
shell([nuget, 'install'] + nuget_args) call([nuget, 'install', *nuget_args])
return installation_path return installation_path
def install_pypy(version: str, arch: str, url: str) -> str: def install_pypy(version: str, arch: str, url: str) -> Path:
assert arch == '32' assert arch == '32'
# Inside the PyPy zip file is a directory with the same name # Inside the PyPy zip file is a directory with the same name
zip_filename = url.rsplit('/', 1)[-1] zip_filename = url.rsplit('/', 1)[-1]
installation_path = os.path.join('C:\\cibw', os.path.splitext(zip_filename)[0]) extension = ".zip"
if not os.path.exists(installation_path): assert zip_filename.endswith(extension)
pypy_zip = os.path.join('C:\\cibw', zip_filename) installation_path = Path('C:\\cibw') / zip_filename[:-len(extension)]
if not installation_path.exists():
pypy_zip = Path('C:\\cibw') / zip_filename
download(url, pypy_zip) download(url, pypy_zip)
# Extract to the parent directory because the zip file still contains a directory # Extract to the parent directory because the zip file still contains a directory
extract_zip(pypy_zip, os.path.dirname(installation_path)) extract_zip(pypy_zip, installation_path.parent)
pypy_exe = 'pypy3.exe' if version[0] == '3' else 'pypy.exe' pypy_exe = 'pypy3.exe' if version[0] == '3' else 'pypy.exe'
shell(['mklink', os.path.join(installation_path, 'python.exe'), os.path.join(installation_path, pypy_exe)]) (installation_path / 'python.exe').symlink_to(installation_path / pypy_exe)
return installation_path return installation_path
def setup_python(python_configuration: PythonConfiguration, dependency_constraint_flags: List[str], environment: ParsedEnvironment) -> Dict[str, str]: def setup_python(python_configuration: PythonConfiguration, dependency_constraint_flags: Sequence[Union[str, PathLike]], environment: ParsedEnvironment) -> Dict[str, str]:
nuget = 'C:\\cibw\\nuget.exe' nuget = Path('C:\\cibw\\nuget.exe')
if not os.path.exists(nuget): if not nuget.exists():
download('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', nuget) download('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', nuget)
if python_configuration.identifier.startswith('cp'): if python_configuration.identifier.startswith('cp'):
@@ -113,58 +118,88 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
else: else:
raise ValueError("Unknown Python implementation") raise ValueError("Unknown Python implementation")
assert os.path.exists(os.path.join(installation_path, 'python.exe')) assert (installation_path / 'python.exe').exists()
# set up PATH and environment variables for run_with_env # set up PATH and environment variables for run_with_env
env = os.environ.copy() env = os.environ.copy()
env['PYTHON_VERSION'] = python_configuration.version env['PYTHON_VERSION'] = python_configuration.version
env['PYTHON_ARCH'] = python_configuration.arch env['PYTHON_ARCH'] = python_configuration.arch
env['PATH'] = os.pathsep.join([ env['PATH'] = os.pathsep.join([
installation_path, str(installation_path),
os.path.join(installation_path, 'Scripts'), str(installation_path / 'Scripts'),
env['PATH'] env['PATH']
]) ])
# update env with results from CIBW_ENVIRONMENT # update env with results from CIBW_ENVIRONMENT
env = environment.as_dictionary(prev_environment=env) env = environment.as_dictionary(prev_environment=env)
# for the logs - check we're running the right version of python # for the logs - check we're running the right version of python
shell(['where', 'python'], env=env) call(['where', 'python'], env=env)
shell(['python', '--version'], env=env) call(['python', '--version'], env=env)
shell(['python', '-c', '"import struct; print(struct.calcsize(\'P\') * 8)"'], env=env) call(['python', '-c', '"import struct; print(struct.calcsize(\'P\') * 8)"'], env=env)
where_python = subprocess.check_output(['where', 'python'], env=env, universal_newlines=True).splitlines()[0].strip() where_python = subprocess.check_output(['where', 'python'], env=env, universal_newlines=True).splitlines()[0].strip()
if where_python != os.path.join(installation_path, 'python.exe'): if where_python != str(installation_path / 'python.exe'):
print("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.", file=sys.stderr) print("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.", file=sys.stderr)
exit(1) exit(1)
# make sure pip is installed # make sure pip is installed
if not os.path.exists(os.path.join(installation_path, 'Scripts', 'pip.exe')): if not (installation_path / 'Scripts' / 'pip.exe').exists():
shell(['python', get_pip_script] + dependency_constraint_flags, env=env, cwd="C:\\cibw") call(['python', get_pip_script, *dependency_constraint_flags], env=env, cwd="C:\\cibw")
assert os.path.exists(os.path.join(installation_path, 'Scripts', 'pip.exe')) assert (installation_path / 'Scripts' / 'pip.exe').exists()
where_pip = subprocess.check_output(['where', 'pip'], env=env, universal_newlines=True).splitlines()[0].strip() where_pip = subprocess.check_output(['where', 'pip'], env=env, universal_newlines=True).splitlines()[0].strip()
if where_pip.strip() != os.path.join(installation_path, 'Scripts', 'pip.exe'): if where_pip.strip() != str(installation_path / 'Scripts' / 'pip.exe'):
print("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.", file=sys.stderr) print("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.", file=sys.stderr)
exit(1) exit(1)
# prepare the Python environment # prepare the Python environment
shell(['python', '-m', 'pip', 'install', '--upgrade', 'pip'] + dependency_constraint_flags, env=env) call(['python', '-m', 'pip', 'install', '--upgrade', 'pip', *dependency_constraint_flags], env=env)
shell(['pip', '--version'], env=env) call(['pip', '--version'], env=env)
shell(['pip', 'install', '--upgrade', 'setuptools', 'wheel'] + dependency_constraint_flags, env=env) call(['pip', 'install', '--upgrade', 'setuptools', 'wheel', *dependency_constraint_flags], env=env)
return env return env
def pep_518_cp35_workaround(package_dir: Path, env: Dict[str, str]) -> None:
"""
Python 3.5 PEP 518 hack (see https://github.com/pypa/pip/issues/8392#issuecomment-639563494)
Basically, nuget's Python is an embedded Python distribution, which is not supported by pip.
Before version 3.6, there was no way to disable the "embedded" behavior, including the ignoring
of environment variables, including the ones pip uses to setup PEP 518 builds.
The fix here is as suggested in that issue; we manually setup the PEP 518 requirements. Since we
are in a fresh environment (except for pinned cibuildweel dependencies), the build is already
mostly "isolated".
"""
pyproject_path = package_dir / 'pyproject.toml'
if pyproject_path.exists():
data = toml.load(pyproject_path)
requirements = (
data['build-system'].get('requires', [])
if 'build-system' in data
else []
)
if requirements:
shell(['pip', 'install'] + requirements, env=env)
def build(options: BuildOptions) -> None: def build(options: BuildOptions) -> None:
temp_dir = tempfile.mkdtemp(prefix='cibuildwheel') temp_dir = Path(tempfile.mkdtemp(prefix='cibuildwheel'))
built_wheel_dir = os.path.join(temp_dir, 'built_wheel') built_wheel_dir = temp_dir / 'built_wheel'
repaired_wheel_dir = os.path.join(temp_dir, 'repaired_wheel') repaired_wheel_dir = temp_dir / 'repaired_wheel'
# install nuget as best way to provide python # install nuget as best way to provide python
nuget = 'C:\\cibw\\nuget.exe' nuget = Path('C:\\cibw\\nuget.exe')
download('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', nuget) download('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', nuget)
if options.before_all:
env = options.environment.as_dictionary(prev_environment=os.environ)
before_all_prepared = prepare_command(options.before_all, project='.', package=options.package_dir)
shell(before_all_prepared, env=env)
python_configurations = get_python_configurations(options.build_selector) python_configurations = get_python_configurations(options.build_selector)
for config in python_configurations: for config in python_configurations:
dependency_constraint_flags = [] dependency_constraint_flags: Sequence[Union[str, PathLike]] = []
if options.dependency_constraints: if options.dependency_constraints:
dependency_constraint_flags = [ dependency_constraint_flags = [
'-c', options.dependency_constraints.get_for_python_version(config.version) '-c', options.dependency_constraints.get_for_python_version(config.version)
@@ -176,47 +211,59 @@ def build(options: BuildOptions) -> None:
# run the before_build command # run the before_build command
if options.before_build: if options.before_build:
before_build_prepared = prepare_command(options.before_build, project='.', package=options.package_dir) before_build_prepared = prepare_command(options.before_build, project='.', package=options.package_dir)
shell([before_build_prepared], env=env) shell(before_build_prepared, env=env)
# activate the PEP 518 patch if on Windows Python 3.5
# (will only have an effect if PEP 517 builds are used):
if config.version.startswith('3.5'):
pep_518_cp35_workaround(options.package_dir, env)
# build the wheel # build the wheel
if os.path.exists(built_wheel_dir): if built_wheel_dir.exists():
shutil.rmtree(built_wheel_dir) shutil.rmtree(built_wheel_dir)
os.makedirs(built_wheel_dir) built_wheel_dir.mkdir(parents=True)
# os.path.abspath is need. Without it pip wheel may try to fetch package from pypi.org # Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/joerick/cibuildwheel/pull/369 # see https://github.com/joerick/cibuildwheel/pull/369
shell(['pip', 'wheel', os.path.abspath(options.package_dir), '-w', built_wheel_dir, '--no-deps'] + get_build_verbosity_extra_flags(options.build_verbosity), env=env) call([
built_wheel = glob(os.path.join(built_wheel_dir, '*.whl'))[0] 'pip', 'wheel',
options.package_dir.resolve(),
'-w', built_wheel_dir,
'--no-deps',
*get_build_verbosity_extra_flags(options.build_verbosity)
], env=env)
built_wheel = next(built_wheel_dir.glob('*.whl'))
# repair the wheel # repair the wheel
if os.path.exists(repaired_wheel_dir): if repaired_wheel_dir.exists():
shutil.rmtree(repaired_wheel_dir) shutil.rmtree(repaired_wheel_dir)
os.makedirs(repaired_wheel_dir) repaired_wheel_dir.mkdir(parents=True)
if built_wheel.endswith('none-any.whl') or not options.repair_command: if built_wheel.name.endswith('none-any.whl') or not options.repair_command:
# pure Python wheel or empty repair command # pure Python wheel or empty repair command
shutil.move(built_wheel, repaired_wheel_dir) shutil.move(str(built_wheel), repaired_wheel_dir)
else: else:
repair_command_prepared = prepare_command(options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir) repair_command_prepared = prepare_command(options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir)
shell([repair_command_prepared], env=env) shell(repair_command_prepared, env=env)
repaired_wheel = glob(os.path.join(repaired_wheel_dir, '*.whl'))[0] repaired_wheel = next(repaired_wheel_dir.glob('*.whl'))
if options.test_command: if options.test_command:
# set up a virtual environment to install and test from, to make sure # set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time. # there are no dependencies that were pulled in at build time.
shell(['pip', 'install', 'virtualenv'] + dependency_constraint_flags, env=env) call(['pip', 'install', 'virtualenv', *dependency_constraint_flags], env=env)
venv_dir = tempfile.mkdtemp() venv_dir = Path(tempfile.mkdtemp())
# Use --no-download to ensure determinism by using seed libraries # Use --no-download to ensure determinism by using seed libraries
# built into virtualenv # built into virtualenv
shell(['python', '-m', 'virtualenv', '--no-download', venv_dir], env=env) call(['python', '-m', 'virtualenv', '--no-download', venv_dir], env=env)
virtualenv_env = env.copy() virtualenv_env = env.copy()
virtualenv_env['PATH'] = os.pathsep.join([ virtualenv_env['PATH'] = os.pathsep.join([
os.path.join(venv_dir, 'Scripts'), str(venv_dir / 'Scripts'),
virtualenv_env['PATH'], virtualenv_env['PATH'],
]) ])
# check that we are using the Python from the virtual environment # check that we are using the Python from the virtual environment
shell(['which', 'python'], env=virtualenv_env) call(['which', 'python'], env=virtualenv_env)
if options.before_test: if options.before_test:
before_test_prepared = prepare_command( before_test_prepared = prepare_command(
@@ -224,30 +271,27 @@ def build(options: BuildOptions) -> None:
project='.', project='.',
package=options.package_dir package=options.package_dir
) )
shell([before_test_prepared], env=virtualenv_env) shell(before_test_prepared, env=virtualenv_env)
# install the wheel # install the wheel
shell(['pip', 'install', repaired_wheel + options.test_extras], env=virtualenv_env) call(['pip', 'install', str(repaired_wheel) + options.test_extras], env=virtualenv_env)
# test the wheel # test the wheel
if options.test_requires: if options.test_requires:
shell(['pip', 'install'] + options.test_requires, env=virtualenv_env) call(['pip', 'install'] + options.test_requires, env=virtualenv_env)
# run the tests from c:\, with an absolute path in the command # run the tests from c:\, with an absolute path in the command
# (this ensures that Python runs the tests against the installed wheel # (this ensures that Python runs the tests against the installed wheel
# and not the repo code) # and not the repo code)
test_command_prepared = prepare_command( test_command_prepared = prepare_command(
options.test_command, options.test_command,
project=os.path.abspath('.'), project=Path('.').resolve(),
package=os.path.abspath(options.package_dir) package=options.package_dir.resolve()
) )
shell([test_command_prepared], cwd='c:\\', env=virtualenv_env) shell(test_command_prepared, cwd='c:\\', env=virtualenv_env)
# clean up # clean up
shutil.rmtree(venv_dir) shutil.rmtree(venv_dir)
# we're all done here; move it to output (remove if already exists) # we're all done here; move it to output (remove if already exists)
dst = os.path.join(options.output_dir, os.path.basename(repaired_wheel)) shutil.move(str(repaired_wheel), options.output_dir)
if os.path.isfile(dst):
os.remove(dst)
shutil.move(repaired_wheel, dst)
+1 -1
View File
@@ -33,4 +33,4 @@ Obviously, manual steps are for chumps, so we can automate this a little by usin
If you don't need much control over the release of a package, you can set up cibuildwheel to deliver the wheels straight to PyPI. This doesn't require anycloud storage to work - you just need to bump the version and tag it. If you don't need much control over the release of a package, you can set up cibuildwheel to deliver the wheels straight to PyPI. This doesn't require anycloud storage to work - you just need to bump the version and tag it.
[`examples/travis-deploy.yml`](https://github.com/joerick/cibuildwheel/blob/master/examples/travis-deploy.yml) and [`examples/github-deploy.yml`](https://github.com/joerick/cibuildwheel/blob/master/examples/travis-deploy.yml) are example configurations that automatocially upload wheels to PyPI. Also check out [this example repo](https://github.com/joerick/cibuildwheel-autopypi-example) for more detailed instructions on how to set this up. [`examples/travis-ci-deploy.yml`](https://github.com/joerick/cibuildwheel/blob/master/examples/travis-ci-deploy.yml) and [`examples/github-deploy.yml`](https://github.com/joerick/cibuildwheel/blob/master/examples/github-deploy.yml) are example configurations that automatically upload wheels to PyPI. Also check out [this example repo](https://github.com/joerick/cibuildwheel-autopypi-example) for more detailed instructions on how to set this up.
-1
View File
@@ -2,4 +2,3 @@
$('.rst-content pre') $('.rst-content pre')
.prev('blockquote') .prev('blockquote')
.addClass('code-block-filename'); .addClass('code-block-filename');
@@ -1,7 +1,6 @@
import cgi import cgi
import io
import os
import re import re
from pathlib import Path
import mkdocs import mkdocs
@@ -41,13 +40,12 @@ class ImportMarkdownPlugin(mkdocs.plugins.BasePlugin):
def found_include_tag(match): def found_include_tag(match):
filename = match.group('filename') filename = match.group('filename')
file_path_abs = os.path.join(os.path.dirname(page_src_path), filename) file_path_abs = Path(page_src_path).parent / filename
if not os.path.exists(file_path_abs): if not file_path_abs.exists():
raise ValueError('file not found', filename) raise ValueError('file not found', filename)
with io.open(file_path_abs, encoding='utf8') as f: text_to_include = file_path_abs.read_text(encoding='utf8')
text_to_include = f.read()
# Allow good practice of having a final newline in the file # Allow good practice of having a final newline in the file
if text_to_include.endswith('\n'): if text_to_include.endswith('\n'):
@@ -60,13 +58,12 @@ class ImportMarkdownPlugin(mkdocs.plugins.BasePlugin):
start = match.group('start') start = match.group('start')
end = match.group('end') end = match.group('end')
file_path_abs = os.path.join(os.path.dirname(page_src_path), filename) file_path_abs = Path(page_src_path).parent / filename
if not os.path.exists(file_path_abs): if not file_path_abs.exists():
raise ValueError('file not found', filename) raise ValueError('file not found', filename)
with io.open(file_path_abs, encoding='utf8') as f: text_to_include = file_path_abs.read_text(encoding='utf8')
text_to_include = f.read()
if start: if start:
_, _, text_to_include = text_to_include.partition(start) _, _, text_to_include = text_to_include.partition(start)
+23 -1
View File
@@ -190,6 +190,28 @@ CIBW_ENVIRONMENT: "BUILD_TIME=$(date) SAMPLE_TEXT=\"sample text\""
!!! note !!! note
`cibuildwheel` always defines the environment variable `CIBUILDWHEEL=1`. This can be useful for [building wheels with optional extensions](faq.md#building-packages-with-optional-c-extensions). `cibuildwheel` always defines the environment variable `CIBUILDWHEEL=1`. This can be useful for [building wheels with optional extensions](faq.md#building-packages-with-optional-c-extensions).
### `CIBW_BEFORE_ALL` {: #before-all}
> Execute a shell command on the build system before any wheels are built.
Shell command to prepare a common part of the project (e.g. build or install libraries which does not depend on the specific version of Python).
This option is very useful for the Linux build, where builds take place in isolated Docker containers managed by cibuildwheel. This command will run inside the container before the wheel builds start. Note, if you're building both x86_64 and i686 wheels (the default), your build uses two different Docker images. In that case, this command will execute twice - once per build container.
The placeholder `{package}` can be used here; it will be replaced by the path to the package being built by `cibuildwheel`.
On Windows and macOS, the version of Python available inside `CIBW_BEFORE_ALL` is whatever is available on the host machine. On Linux, a modern Python version is available on PATH.
Platform-specific variants also available:<br/>
`CIBW_BEFORE_ALL_MACOS` | `CIBW_BEFORE_ALL_WINDOWS` | `CIBW_BEFORE_ALL_LINUX`
#### Examples
```yaml
# build third party library
CIBW_BEFORE_ALL: make -C third_party_lib
# install system library
CIBW_BEFORE_ALL_LINUX: yum install -y libffi-dev
```
### `CIBW_BEFORE_BUILD` {: #before-build} ### `CIBW_BEFORE_BUILD` {: #before-build}
> Execute a shell command preparing each wheel's build > Execute a shell command preparing each wheel's build
@@ -603,7 +625,7 @@ optional arguments:
var url = 'https://cibuildwheel.readthedocs.io/en/stable/options/#'+option.id; var url = 'https://cibuildwheel.readthedocs.io/en/stable/options/#'+option.id;
var namesMarkdown = $.map(optionNames, function(n) { var namesMarkdown = $.map(optionNames, function(n) {
return '[`'+n+'`]('+url+') ' return '[`'+n+'`]('+url+') '
}).join(' ') }).join(' <br> ')
markdown += '| '+namesMarkdown+' ' markdown += '| '+namesMarkdown+' '
markdown += '| '+option.description.trim()+' ' markdown += '| '+option.description.trim()+' '
+1 -1
View File
@@ -12,7 +12,7 @@ stack: python 3.7
init: init:
- cmd: set PATH=C:\Python37;C:\Python37\Scripts;%PATH% - cmd: set PATH=C:\Python37;C:\Python37\Scripts;%PATH%
install: python -m pip install cibuildwheel==1.4.2 install: python -m pip install cibuildwheel==1.5.5
build_script: python -m cibuildwheel --output-dir wheelhouse build_script: python -m cibuildwheel --output-dir wheelhouse
+3 -3
View File
@@ -5,7 +5,7 @@ jobs:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
- bash: | - bash: |
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
pip3 install cibuildwheel==1.4.2 pip3 install cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse . cibuildwheel --output-dir wheelhouse .
- task: PublishBuildArtifacts@1 - task: PublishBuildArtifacts@1
inputs: {pathtoPublish: 'wheelhouse'} inputs: {pathtoPublish: 'wheelhouse'}
@@ -16,7 +16,7 @@ jobs:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
- bash: | - bash: |
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
pip3 install cibuildwheel==1.4.2 pip3 install cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse . cibuildwheel --output-dir wheelhouse .
- task: PublishBuildArtifacts@1 - task: PublishBuildArtifacts@1
inputs: {pathtoPublish: 'wheelhouse'} inputs: {pathtoPublish: 'wheelhouse'}
@@ -29,7 +29,7 @@ jobs:
displayName: Install Visual C++ for Python 2.7 displayName: Install Visual C++ for Python 2.7
- bash: | - bash: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install cibuildwheel==1.4.2 pip install cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse . cibuildwheel --output-dir wheelhouse .
- task: PublishBuildArtifacts@1 - task: PublishBuildArtifacts@1
inputs: {pathtoPublish: 'wheelhouse'} inputs: {pathtoPublish: 'wheelhouse'}
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
- run: - run:
name: Build the Linux wheels. name: Build the Linux wheels.
command: | command: |
pip3 install --user cibuildwheel==1.4.2 pip3 install --user cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse cibuildwheel --output-dir wheelhouse
- store_artifacts: - store_artifacts:
path: wheelhouse/ path: wheelhouse/
@@ -25,7 +25,7 @@ jobs:
- run: - run:
name: Build the OS X wheels. name: Build the OS X wheels.
command: | command: |
pip3 install --user cibuildwheel==1.4.2 pip3 install --user cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse cibuildwheel --output-dir wheelhouse
- store_artifacts: - store_artifacts:
path: wheelhouse/ path: wheelhouse/
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Install cibuildwheel - name: Install cibuildwheel
run: | run: |
python -m pip install cibuildwheel==1.4.2 python -m pip install cibuildwheel==1.5.5
- name: Install Visual C++ for Python 2.7 - name: Install Visual C++ for Python 2.7
if: runner.os == 'Windows' if: runner.os == 'Windows'
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Install cibuildwheel - name: Install cibuildwheel
run: | run: |
python -m pip install cibuildwheel==1.4.2 python -m pip install cibuildwheel==1.5.5
- name: Install Visual C++ for Python 2.7 - name: Install Visual C++ for Python 2.7
if: runner.os == 'Windows' if: runner.os == 'Windows'
+1 -1
View File
@@ -25,7 +25,7 @@ env:
# Note: TWINE_PASSWORD is set to a PyPI API token in Travis settings # Note: TWINE_PASSWORD is set to a PyPI API token in Travis settings
install: install:
- python3 -m pip install cibuildwheel==1.4.2 - python3 -m pip install cibuildwheel==1.5.5
script: script:
# build the wheels, put them into './wheelhouse' # build the wheels, put them into './wheelhouse'
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
- ln -s /c/Python38/python.exe /c/Python38/python3.exe - ln -s /c/Python38/python.exe /c/Python38/python3.exe
install: install:
- python3 -m pip install cibuildwheel==1.4.2 - python3 -m pip install cibuildwheel==1.5.5
script: script:
# build the wheels, put them into './wheelhouse' # build the wheels, put them into './wheelhouse'
+3 -3
View File
@@ -55,7 +55,7 @@ jobs:
- stage: deploy - stage: deploy
name: Build and deploy Linux wheels name: Build and deploy Linux wheels
services: docker services: docker
install: python3 -m pip install cibuildwheel==1.4.2 install: python3 -m pip install cibuildwheel==1.5.5
script: python3 -m cibuildwheel --output-dir wheelhouse script: python3 -m cibuildwheel --output-dir wheelhouse
after_success: | after_success: |
python3 -m pip install twine python3 -m pip install twine
@@ -65,7 +65,7 @@ jobs:
name: Build and deploy macOS wheels name: Build and deploy macOS wheels
os: osx os: osx
language: shell language: shell
install: python3 -m pip install cibuildwheel==1.4.2 install: python3 -m pip install cibuildwheel==1.5.5
script: python3 -m cibuildwheel --output-dir wheelhouse script: python3 -m cibuildwheel --output-dir wheelhouse
after_success: | after_success: |
python3 -m pip install twine python3 -m pip install twine
@@ -75,7 +75,7 @@ jobs:
name: Build and deploy Windows wheels name: Build and deploy Windows wheels
os: windows os: windows
language: shell language: shell
install: python3 -m pip install cibuildwheel==1.4.2 install: python3 -m pip install cibuildwheel==1.5.5
script: python3 -m cibuildwheel --output-dir wheelhouse script: python3 -m cibuildwheel --output-dir wheelhouse
after_success: | after_success: |
python3 -m pip install twine python3 -m pip install twine
+2
View File
@@ -6,3 +6,5 @@ pymdown-extensions
pip-tools pip-tools
requests requests
click click
mypy
typing-extensions
+1 -1
View File
@@ -1,5 +1,5 @@
[flake8] [flake8]
ignore = E501,W503,E741 ignore = E501,W503,E741,E226
application-import-names = cibuildwheel application-import-names = cibuildwheel
exclude = exclude =
cibuildwheel/resources/, cibuildwheel/resources/,
+5 -7
View File
@@ -1,20 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import io from pathlib import Path
import os
try: try:
from setuptools import setup from setuptools import setup
except ImportError: except ImportError:
from distutils.core import setup from distutils.core import setup
this_directory = os.path.dirname(__file__) this_directory = Path(__file__).parent
with io.open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f: long_description = (this_directory / 'README.md').read_text(encoding='utf-8')
long_description = f.read()
setup( setup(
name='cibuildwheel', name='cibuildwheel',
version='1.4.2', version='1.5.5',
install_requires=['bashlex!=0.13'], install_requires=['bashlex!=0.13', 'toml'],
description="Build Python wheels on CI with minimal configuration.", description="Build Python wheels on CI with minimal configuration.",
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
+63
View File
@@ -0,0 +1,63 @@
import pytest
import subprocess
import textwrap
from . import utils
from . import test_projects
project_with_before_build_asserts = test_projects.new_c_project(
setup_py_add=textwrap.dedent(r'''
# assert that the Python version as written to text_info.txt in the CIBW_BEFORE_ALL step
# is the same one as is currently running.
with open("text_info.txt") as f:
stored_text = f.read()
print("## stored text: " + stored_text)
assert stored_text == "sample text 123"
''')
)
def test(tmp_path):
project_dir = tmp_path / 'project'
project_with_before_build_asserts.generate(project_dir)
with (project_dir / 'text_info.txt').open(mode='w') as ff:
print("dummy text", file=ff)
# build the wheels
actual_wheels = utils.cibuildwheel_run(project_dir, add_env={
# write python version information to a temporary file, this is
# checked in setup.py
'CIBW_BEFORE_ALL': '''python -c "import os;open('{project}/text_info.txt', 'w').write('sample text '+os.environ.get('TEST_VAL', ''))"''',
'CIBW_ENVIRONMENT': "TEST_VAL='123'"
})
# also check that we got the right wheels
(project_dir / 'text_info.txt').unlink()
expected_wheels = utils.expected_wheels('spam', '0.1.0')
assert set(actual_wheels) == set(expected_wheels)
def test_failing_command(tmp_path):
project_dir = tmp_path / 'project'
test_projects.new_c_project().generate(project_dir)
with pytest.raises(subprocess.CalledProcessError):
utils.cibuildwheel_run(project_dir, add_env={
'CIBW_BEFORE_ALL': 'false',
'CIBW_BEFORE_ALL_WINDOWS': 'exit /b 1',
})
def test_cwd(tmp_path):
project_dir = tmp_path / 'project'
test_projects.new_c_project().generate(project_dir)
actual_wheels = utils.cibuildwheel_run(project_dir, add_env={
'CIBW_BEFORE_ALL': f'''python -c "import os; assert os.getcwd() == {str(project_dir)!r}"''',
'CIBW_BEFORE_ALL_LINUX': '''python -c "import os; assert os.getcwd() == '/project'"''',
})
expected_wheels = utils.expected_wheels('spam', '0.1.0')
assert set(actual_wheels) == set(expected_wheels)
+31 -2
View File
@@ -1,3 +1,5 @@
import pytest
import subprocess
import textwrap import textwrap
from . import utils from . import utils
@@ -32,14 +34,41 @@ def test(tmp_path):
project_dir = tmp_path / 'project' project_dir = tmp_path / 'project'
project_with_before_build_asserts.generate(project_dir) project_with_before_build_asserts.generate(project_dir)
before_build = ('''python -c "import sys; open('{output_dir}pythonversion.txt', 'w').write(sys.version)" && '''
'''python -c "import sys; open('{output_dir}pythonexecutable.txt', 'w').write(sys.executable)"''')
# build the wheels # build the wheels
actual_wheels = utils.cibuildwheel_run(project_dir, add_env={ actual_wheels = utils.cibuildwheel_run(project_dir, add_env={
# write python version information to a temporary file, this is # write python version information to a temporary file, this is
# checked in setup.py # checked in setup.py
'CIBW_BEFORE_BUILD': '''python -c "import sys; open('/tmp/pythonversion.txt', 'w').write(sys.version)" && python -c "import sys; open('/tmp/pythonexecutable.txt', 'w').write(sys.executable)"''', 'CIBW_BEFORE_BUILD': before_build.format(output_dir='/tmp/'),
'CIBW_BEFORE_BUILD_WINDOWS': '''python -c "import sys; open('c:\\pythonversion.txt', 'w').write(sys.version)" && python -c "import sys; open('c:\\pythonexecutable.txt', 'w').write(sys.executable)"''', 'CIBW_BEFORE_BUILD_WINDOWS': before_build.format(output_dir=r'c:\\'),
}) })
# also check that we got the right wheels # also check that we got the right wheels
expected_wheels = utils.expected_wheels('spam', '0.1.0') expected_wheels = utils.expected_wheels('spam', '0.1.0')
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
def test_failing_command(tmp_path):
project_dir = tmp_path / 'project'
test_projects.new_c_project().generate(project_dir)
with pytest.raises(subprocess.CalledProcessError):
utils.cibuildwheel_run(project_dir, add_env={
'CIBW_BEFORE_BUILD': 'false',
'CIBW_BEFORE_BUILD_WINDOWS': 'exit /b 1',
})
def test_cwd(tmp_path):
project_dir = tmp_path / 'project'
test_projects.new_c_project().generate(project_dir)
actual_wheels = utils.cibuildwheel_run(project_dir, add_env={
'CIBW_BEFORE_BUILD': f'''python -c "import os; assert os.getcwd() == {str(project_dir)!r}"''',
'CIBW_BEFORE_BUILD_LINUX': '''python -c "import os; assert os.getcwd() == '/project'"''',
})
expected_wheels = utils.expected_wheels('spam', '0.1.0')
assert set(actual_wheels) == set(expected_wheels)
+2 -5
View File
@@ -1,4 +1,3 @@
import os
import re import re
import pytest import pytest
import textwrap import textwrap
@@ -39,11 +38,9 @@ VERSION_REGEX = r'([\w-]+)==([^\s]+)'
def get_versions_from_constraint_file(constraint_file): def get_versions_from_constraint_file(constraint_file):
with open(constraint_file, encoding='utf8') as f: constraint_file_text = constraint_file.read_text(encoding='utf8')
constraint_file_text = f.read()
versions = {} versions = {}
for package, version in re.findall(VERSION_REGEX, constraint_file_text): for package, version in re.findall(VERSION_REGEX, constraint_file_text):
versions[package] = version versions[package] = version
@@ -73,7 +70,7 @@ def test_pinned_versions(tmp_path, python_version):
constraint_filename = 'constraints.txt' constraint_filename = 'constraints.txt'
build_pattern = '[cp]p38-*' build_pattern = '[cp]p38-*'
constraint_file = os.path.join(cibuildwheel.util.resources_dir, constraint_filename) constraint_file = cibuildwheel.util.resources_dir / constraint_filename
constraint_versions = get_versions_from_constraint_file(constraint_file) constraint_versions = get_versions_from_constraint_file(constraint_file)
for package in ['pip', 'setuptools', 'wheel', 'virtualenv']: for package in ['pip', 'setuptools', 'wheel', 'virtualenv']:
+19 -3
View File
@@ -13,12 +13,15 @@ project_with_environment_asserts = test_projects.new_c_project(
# explode if environment isn't correct, as set in CIBW_ENVIRONMENT # explode if environment isn't correct, as set in CIBW_ENVIRONMENT
CIBW_TEST_VAR = os.environ.get("CIBW_TEST_VAR") CIBW_TEST_VAR = os.environ.get("CIBW_TEST_VAR")
CIBW_TEST_VAR_2 = os.environ.get("CIBW_TEST_VAR_2") CIBW_TEST_VAR_2 = os.environ.get("CIBW_TEST_VAR_2")
CIBW_TEST_VAR_3 = os.environ.get("CIBW_TEST_VAR_3")
PATH = os.environ.get("PATH") PATH = os.environ.get("PATH")
if CIBW_TEST_VAR != "a b c": if CIBW_TEST_VAR != "a b c":
raise Exception('CIBW_TEST_VAR should equal "a b c". It was "%s"' % CIBW_TEST_VAR) raise Exception('CIBW_TEST_VAR should equal "a b c". It was "%s"' % CIBW_TEST_VAR)
if CIBW_TEST_VAR_2 != "1": if CIBW_TEST_VAR_2 != "1":
raise Exception('CIBW_TEST_VAR_2 should equal "1". It was "%s"' % CIBW_TEST_VAR_2) raise Exception('CIBW_TEST_VAR_2 should equal "1". It was "%s"' % CIBW_TEST_VAR_2)
if CIBW_TEST_VAR_3 != "test string 3":
raise Exception('CIBW_TEST_VAR_3 should equal "test string 3". It was "%s"' % CIBW_TEST_VAR_3)
if "/opt/cibw_test_path" not in PATH: if "/opt/cibw_test_path" not in PATH:
raise Exception('PATH should contain "/opt/cibw_test_path". It was "%s"' % PATH) raise Exception('PATH should contain "/opt/cibw_test_path". It was "%s"' % PATH)
if "$PATH" in PATH: if "$PATH" in PATH:
@@ -44,7 +47,7 @@ def test(tmp_path):
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
def test_overridden_path(tmp_path): def test_overridden_path(tmp_path, capfd):
project_dir = tmp_path / 'project' project_dir = tmp_path / 'project'
output_dir = tmp_path / 'output' output_dir = tmp_path / 'output'
@@ -54,8 +57,21 @@ def test_overridden_path(tmp_path):
# mess up PATH, somehow # mess up PATH, somehow
with pytest.raises(subprocess.CalledProcessError): with pytest.raises(subprocess.CalledProcessError):
if utils.platform == 'linux':
utils.cibuildwheel_run(project_dir, output_dir=output_dir, add_env={ utils.cibuildwheel_run(project_dir, output_dir=output_dir, add_env={
'CIBW_ENVIRONMENT': '''SOMETHING="$(mkdir new_path && touch new_path/python)" PATH="$(realpath new_path):$PATH"''', 'CIBW_BEFORE_ALL': 'mkdir new_path && touch new_path/python && chmod +x new_path/python',
'CIBW_ENVIRONMENT_WINDOWS': '''SOMETHING="$(mkdir new_path && type nul > new_path/python.exe)" PATH="$CD\\new_path;$PATH"''', 'CIBW_ENVIRONMENT': '''PATH="$(pwd)/new_path:$PATH"''',
}) })
else:
new_path = tmp_path / 'another_bin'
new_path.mkdir()
(new_path / 'python').touch(mode=0o777)
utils.cibuildwheel_run(project_dir, output_dir=output_dir, add_env={
'NEW_PATH': str(new_path),
'CIBW_ENVIRONMENT': f'''PATH="$NEW_PATH{os.pathsep}$PATH"''',
})
assert len(os.listdir(output_dir)) == 0 assert len(os.listdir(output_dir)) == 0
captured = capfd.readouterr()
assert "python available on PATH doesn't match our installed instance" in captured.err
+49
View File
@@ -0,0 +1,49 @@
import textwrap
from . import test_projects
from . import utils
basic_project = test_projects.new_c_project(
setup_py_add=textwrap.dedent(
"""
# Will fail if PEP 518 does work
import sys
import requests
if sys.version_info < (3, 6, 0):
assert requests.__version__ == "2.22.0", "Requests found but wrong version ({0})".format(requests.__version__)
else:
assert requests.__version__ == "2.23.0", "Requests found but wrong version ({0})".format(requests.__version__)
# Just making sure environment is still set
import os
if os.environ.get("CIBUILDWHEEL", "0") != "1":
raise Exception("CIBUILDWHEEL environment variable is not set to 1")
"""
)
)
basic_project.files[
"pyproject.toml"
] = """
[build-system]
requires = [
"setuptools >= 42",
"wheel",
"requests==2.22.0; python_version<'3.6'",
"requests==2.23.0; python_version>='3.6'"
]
build-backend = "setuptools.build_meta"
"""
def test_pep518(tmp_path):
project_dir = tmp_path / "project"
basic_project.generate(project_dir)
# build the wheels
actual_wheels = utils.cibuildwheel_run(project_dir)
# check that the expected wheels are produced
expected_wheels = utils.expected_wheels("spam", "0.1.0")
assert set(actual_wheels) == set(expected_wheels)
+5 -4
View File
@@ -1,8 +1,9 @@
from argparse import ArgumentParser
import importlib import importlib
import tempfile
import sys
import subprocess import subprocess
import sys
import tempfile
from argparse import ArgumentParser
from pathlib import Path
def main(): def main():
@@ -19,7 +20,7 @@ def main():
project = getattr(importlib.import_module(module), name) project = getattr(importlib.import_module(module), name)
project_dir = tempfile.mkdtemp() project_dir = Path(tempfile.mkdtemp())
project.generate(project_dir) project.generate(project_dir)
print('Project generated at', project_dir) print('Project generated at', project_dir)
+7 -5
View File
@@ -1,5 +1,7 @@
import os from pathlib import Path
import jinja2 import jinja2
from typing import Union, Dict, Any from typing import Union, Dict, Any
@@ -23,12 +25,12 @@ class TestProject:
self.files = {} self.files = {}
self.template_context = {} self.template_context = {}
def generate(self, path: str): def generate(self, path: Path):
for filename, content in self.files.items(): for filename, content in self.files.items():
file_path = os.path.join(path, filename) file_path = path / filename
os.makedirs(os.path.dirname(file_path), exist_ok=True) file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, 'w', encoding='utf8') as f: with file_path.open('w', encoding='utf8') as f:
if isinstance(content, jinja2.Template): if isinstance(content, jinja2.Template):
content = content.render(self.template_context) content = content.render(self.template_context)
+2 -2
View File
@@ -1,4 +1,4 @@
import os from pathlib import Path
import jinja2 import jinja2
@@ -35,7 +35,7 @@ def test(capfd, tmp_path):
project_dir = tmp_path / 'project' project_dir = tmp_path / 'project'
subdir_package_project.generate(project_dir) subdir_package_project.generate(project_dir)
package_dir = os.path.join('src', 'spam') package_dir = Path('src', 'spam')
# build the wheels # build the wheels
actual_wheels = utils.cibuildwheel_run(project_dir, package_dir=package_dir, add_env={ actual_wheels = utils.cibuildwheel_run(project_dir, package_dir=package_dir, add_env={
'CIBW_BEFORE_BUILD': 'python {project}/bin/before_build.py', 'CIBW_BEFORE_BUILD': 'python {project}/bin/before_build.py',
+3 -2
View File
@@ -10,9 +10,10 @@ import shutil
import subprocess import subprocess
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path
from tempfile import mkdtemp from tempfile import mkdtemp
IS_WINDOWS_RUNNING_ON_AZURE = os.path.exists('C:\\hostedtoolcache') IS_WINDOWS_RUNNING_ON_AZURE = Path('C:\\hostedtoolcache').exists()
IS_WINDOWS_RUNNING_ON_TRAVIS = os.environ.get('TRAVIS_OS_NAME') == 'windows' IS_WINDOWS_RUNNING_ON_TRAVIS = os.environ.get('TRAVIS_OS_NAME') == 'windows'
@@ -66,7 +67,7 @@ def cibuildwheel_run(project_path, package_dir='.', env=None, add_env=None, outp
with TemporaryDirectoryIfNone(output_dir) as _output_dir: with TemporaryDirectoryIfNone(output_dir) as _output_dir:
subprocess.check_call( subprocess.check_call(
[sys.executable, '-m', 'cibuildwheel', '--output-dir', str(_output_dir), package_dir], [sys.executable, '-m', 'cibuildwheel', '--output-dir', str(_output_dir), str(package_dir)],
env=env, env=env,
cwd=project_path, cwd=project_path,
) )
+21
View File
@@ -0,0 +1,21 @@
import pytest
def pytest_addoption(parser):
parser.addoption(
"--run-docker", action="store_true", default=False, help="run docker tests"
)
def pytest_configure(config):
config.addinivalue_line("markers", "docker: mark test requiring docker to run")
def pytest_collection_modifyitems(config, items):
if config.getoption("--run-docker"):
# --run-docker given in cli: do not skip docker tests
return
skip_docker = pytest.mark.skip(reason="need --run-docker option to run")
for item in items:
if "docker" in item.keywords:
item.add_marker(skip_docker)
+9 -23
View File
@@ -1,30 +1,16 @@
from cibuildwheel.util import DependencyConstraints from cibuildwheel.util import DependencyConstraints
import os
from pathlib import Path
def test_defaults(): def test_defaults():
dependency_constraints = DependencyConstraints.with_defaults() dependency_constraints = DependencyConstraints.with_defaults()
project_root = os.path.dirname(os.path.dirname(__file__)) project_root = Path(__file__).parents[1]
resources_dir = os.path.join(project_root, 'cibuildwheel', 'resources') resources_dir = project_root / 'cibuildwheel' / 'resources'
assert os.path.samefile( assert dependency_constraints.base_file_path.samefile(resources_dir / 'constraints.txt')
dependency_constraints.base_file_path, assert dependency_constraints.get_for_python_version('3.8').samefile(resources_dir / 'constraints.txt')
os.path.join(resources_dir, 'constraints.txt') assert dependency_constraints.get_for_python_version('3.6').samefile(resources_dir / 'constraints-python36.txt')
) assert dependency_constraints.get_for_python_version('3.5').samefile(resources_dir / 'constraints-python35.txt')
assert os.path.samefile( assert dependency_constraints.get_for_python_version('2.7').samefile(resources_dir / 'constraints-python27.txt')
dependency_constraints.get_for_python_version('3.8'),
os.path.join(resources_dir, 'constraints.txt')
)
assert os.path.samefile(
dependency_constraints.get_for_python_version('3.6'),
os.path.join(resources_dir, 'constraints-python36.txt')
)
assert os.path.samefile(
dependency_constraints.get_for_python_version('3.5'),
os.path.join(resources_dir, 'constraints-python35.txt')
)
assert os.path.samefile(
dependency_constraints.get_for_python_version('2.7'),
os.path.join(resources_dir, 'constraints-python27.txt')
)
+162
View File
@@ -0,0 +1,162 @@
import platform
import random
import shutil
import subprocess
import textwrap
from pathlib import Path, PurePath
import pytest
from cibuildwheel.docker_container import DockerContainer
from cibuildwheel.environment import EnvironmentAssignment
# for these tests we use manylinux2014 images, because they're available on
# multi architectures and include python3.8
pm = platform.machine()
if pm == "x86_64":
DEFAULT_IMAGE = 'quay.io/pypa/manylinux2014_x86_64:2020-05-17-2f8ac3b'
elif pm == "aarch64":
DEFAULT_IMAGE = 'quay.io/pypa/manylinux2014_aarch64:2020-05-17-2f8ac3b'
elif pm == "ppc64le":
DEFAULT_IMAGE = 'quay.io/pypa/manylinux2014_ppc64le:2020-05-17-2f8ac3b'
elif pm == "s390x":
DEFAULT_IMAGE = 'quay.io/pypa/manylinux2014_s390x:2020-05-17-2f8ac3b'
@pytest.mark.docker
def test_simple():
with DockerContainer(DEFAULT_IMAGE) as container:
assert container.call(['echo', 'hello'], capture_output=True) == 'hello\n'
@pytest.mark.docker
def test_no_lf():
with DockerContainer(DEFAULT_IMAGE) as container:
assert container.call(['printf', 'hello'], capture_output=True) == 'hello'
@pytest.mark.docker
def test_environment():
with DockerContainer(DEFAULT_IMAGE) as container:
assert container.call(['sh', '-c', 'echo $TEST_VAR'], env={'TEST_VAR': '1'}, capture_output=True) == '1\n'
@pytest.mark.docker
def test_cwd():
with DockerContainer(DEFAULT_IMAGE, cwd='/cibuildwheel/working_directory') as container:
assert container.call(['pwd'], capture_output=True) == '/cibuildwheel/working_directory\n'
assert container.call(['pwd'], capture_output=True, cwd='/opt') == '/opt\n'
@pytest.mark.docker
def test_container_removed():
with DockerContainer(DEFAULT_IMAGE) as container:
docker_containers_listing = subprocess.run('docker container ls', shell=True, check=True, stdout=subprocess.PIPE, universal_newlines=True).stdout
assert container.name in docker_containers_listing
old_container_name = container.name
docker_containers_listing = subprocess.run('docker container ls', shell=True, check=True, stdout=subprocess.PIPE, universal_newlines=True).stdout
assert old_container_name not in docker_containers_listing
@pytest.mark.docker
def test_large_environment():
# max environment variable size is 128kB
long_env_var_length = 127*1024
large_environment = {
'a': '0'*long_env_var_length,
'b': '0'*long_env_var_length,
'c': '0'*long_env_var_length,
'd': '0'*long_env_var_length,
}
with DockerContainer(DEFAULT_IMAGE) as container:
# check the length of d
assert container.call(['sh', '-c', 'echo ${#d}'], env=large_environment, capture_output=True) == f'{long_env_var_length}\n'
@pytest.mark.docker
def test_binary_output():
with DockerContainer(DEFAULT_IMAGE) as container:
# note: the below embedded snippets are in python2
# check that we can pass though arbitrary binary data without erroring
container.call(['/usr/bin/python2', '-c', textwrap.dedent('''
import sys
sys.stdout.write(''.join(chr(n) for n in range(0, 256)))
''')])
# check that we can capture arbitrary binary data
output = container.call(['/usr/bin/python2', '-c', textwrap.dedent('''
import sys
sys.stdout.write(''.join(chr(n % 256) for n in range(0, 512)))
''')], capture_output=True)
data = bytes(output, encoding='utf8', errors='surrogateescape')
for i in range(0, 512):
assert data[i] == i % 256
# check that environment variables can carry binary data, except null characters
# (https://www.gnu.org/software/libc/manual/html_node/Environment-Variables.html)
binary_data = bytes(n for n in range(1, 256))
binary_data_string = str(binary_data, encoding='utf8', errors='surrogateescape')
output = container.call(
['python2', '-c', 'import os, sys; sys.stdout.write(os.environ["TEST_VAR"])'],
env={'TEST_VAR': binary_data_string},
capture_output=True,
)
assert output == binary_data_string
@pytest.mark.docker
def test_file_operations(tmp_path: Path):
with DockerContainer(DEFAULT_IMAGE) as container:
# test copying a file in
test_binary_data = bytes(random.randrange(256) for _ in range(1000))
original_test_file = tmp_path / 'test.dat'
original_test_file.write_bytes(test_binary_data)
dst_file = PurePath('/tmp/test.dat')
container.copy_into(original_test_file, dst_file)
output = container.call(['cat', dst_file], capture_output=True)
assert test_binary_data == bytes(output, encoding='utf8', errors='surrogateescape')
@pytest.mark.docker
def test_dir_operations(tmp_path: Path):
with DockerContainer(DEFAULT_IMAGE) as container:
test_binary_data = bytes(random.randrange(256) for _ in range(1000))
original_test_file = tmp_path / 'test.dat'
original_test_file.write_bytes(test_binary_data)
# test copying a dir in
test_dir = tmp_path / 'test_dir'
test_dir.mkdir()
test_file = test_dir / 'test.dat'
shutil.copyfile(original_test_file, test_file)
dst_dir = PurePath('/tmp/test_dir')
dst_file = dst_dir / 'test.dat'
container.copy_into(test_dir, dst_dir)
output = container.call(['cat', dst_file], capture_output=True)
assert test_binary_data == bytes(output, encoding='utf8', errors='surrogateescape')
# test glob
assert container.glob(dst_dir, '*.dat') == [dst_file]
# test copy dir out
new_test_dir = tmp_path / 'test_dir_new'
container.copy_out(dst_dir, new_test_dir)
assert test_binary_data == (new_test_dir / 'test.dat').read_bytes()
@pytest.mark.docker
def test_environment_executor():
with DockerContainer(DEFAULT_IMAGE) as container:
assignment = EnvironmentAssignment("TEST=$(echo 42)")
assert assignment.evaluated_value({}, container.environment_executor) == "42"
+29
View File
@@ -94,3 +94,32 @@ def test_no_vars_pass_through():
environment_dict = environment_recipe.as_dictionary(prev_environment={'CIBUILDWHEEL': 'awesome'}) environment_dict = environment_recipe.as_dictionary(prev_environment={'CIBUILDWHEEL': 'awesome'})
assert environment_dict == {'CIBUILDWHEEL': 'awesome'} assert environment_dict == {'CIBUILDWHEEL': 'awesome'}
def test_operators_inside_eval():
environment_recipe = parse_environment('SOMETHING="$(echo a; echo b; echo c)"')
# pass the existing process env so PATH is available
environment_dict = environment_recipe.as_dictionary(os.environ.copy())
assert environment_dict.get('SOMETHING') == 'a\nb\nc'
def test_substitution_with_backslash():
environment_recipe = parse_environment('PATH2="somewhere_else;$PATH1"')
# pass the existing process env so PATH is available
environment_dict = environment_recipe.as_dictionary(prev_environment={
'PATH1': 'c:\\folder\\'
})
assert environment_dict.get('PATH2') == 'somewhere_else;c:\\folder\\'
def test_awkwardly_quoted_variable():
environment_recipe = parse_environment('VAR2=something"like this""$VAR1"$VAR1$(echo "theres more")"$(echo "and more!")"')
# pass the existing process env so PATH is available
environment_dict = environment_recipe.as_dictionary({'VAR1': 'but wait'})
assert environment_dict.get('VAR2') == 'somethinglike thisbut waitbut waittheres moreand more!'
+13 -8
View File
@@ -1,6 +1,6 @@
import os
import subprocess import subprocess
import sys import sys
from pathlib import Path
import pytest import pytest
@@ -18,7 +18,7 @@ class ArgsInterceptor:
self.kwargs = kwargs self.kwargs = kwargs
MOCK_PACKAGE_DIR = 'some_package_dir' MOCK_PACKAGE_DIR = Path('some_package_dir')
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -31,28 +31,33 @@ def mock_protection(monkeypatch):
def fail_on_call(*args, **kwargs): def fail_on_call(*args, **kwargs):
raise RuntimeError("This should never be called") raise RuntimeError("This should never be called")
def ignore_call(*args, **kwargs):
pass
monkeypatch.setattr(subprocess, 'Popen', fail_on_call) monkeypatch.setattr(subprocess, 'Popen', fail_on_call)
monkeypatch.setattr(util, 'download', fail_on_call) monkeypatch.setattr(util, 'download', fail_on_call)
monkeypatch.setattr(windows, 'build', fail_on_call) monkeypatch.setattr(windows, 'build', fail_on_call)
monkeypatch.setattr(linux, 'build', fail_on_call) monkeypatch.setattr(linux, 'build', fail_on_call)
monkeypatch.setattr(macos, 'build', fail_on_call) monkeypatch.setattr(macos, 'build', fail_on_call)
monkeypatch.setattr(Path, 'mkdir', ignore_call)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def fake_package_dir(monkeypatch): def fake_package_dir(monkeypatch):
''' '''
Monkey-patch enough for the main() function to run Monkey-patch enough for the main() function to run
''' '''
real_os_path_exists = os.path.exists real_path_exists = Path.exists
def mock_os_path_exists(path): def mock_path_exists(path):
if path == os.path.join(MOCK_PACKAGE_DIR, 'setup.py'): if path == MOCK_PACKAGE_DIR / 'setup.py':
return True return True
else: else:
return real_os_path_exists(path) return real_path_exists(path)
monkeypatch.setattr(os.path, 'exists', mock_os_path_exists) monkeypatch.setattr(Path, 'exists', mock_path_exists)
monkeypatch.setattr(sys, 'argv', ['cibuildwheel', MOCK_PACKAGE_DIR]) monkeypatch.setattr(sys, 'argv', ['cibuildwheel', str(MOCK_PACKAGE_DIR)])
@pytest.fixture(params=['linux', 'macos', 'windows']) @pytest.fixture(params=['linux', 'macos', 'windows'])
+24 -5
View File
@@ -1,5 +1,6 @@
import sys import sys
from fnmatch import fnmatch from fnmatch import fnmatch
from pathlib import Path
import pytest import pytest
@@ -12,9 +13,9 @@ from cibuildwheel.util import BuildSelector
def test_output_dir(platform, intercepted_build_args, monkeypatch): def test_output_dir(platform, intercepted_build_args, monkeypatch):
OUTPUT_DIR = 'some_output_dir' OUTPUT_DIR = Path('some_output_dir')
monkeypatch.setenv('CIBW_OUTPUT_DIR', OUTPUT_DIR) monkeypatch.setenv('CIBW_OUTPUT_DIR', str(OUTPUT_DIR))
main() main()
@@ -24,14 +25,14 @@ def test_output_dir(platform, intercepted_build_args, monkeypatch):
def test_output_dir_default(platform, intercepted_build_args, monkeypatch): def test_output_dir_default(platform, intercepted_build_args, monkeypatch):
main() main()
assert intercepted_build_args.args[0].output_dir == 'wheelhouse' assert intercepted_build_args.args[0].output_dir == Path('wheelhouse')
@pytest.mark.parametrize('also_set_environment', [False, True]) @pytest.mark.parametrize('also_set_environment', [False, True])
def test_output_dir_argument(also_set_environment, platform, intercepted_build_args, monkeypatch): def test_output_dir_argument(also_set_environment, platform, intercepted_build_args, monkeypatch):
OUTPUT_DIR = 'some_output_dir' OUTPUT_DIR = Path('some_output_dir')
monkeypatch.setattr(sys, 'argv', sys.argv + ['--output-dir', OUTPUT_DIR]) monkeypatch.setattr(sys, 'argv', sys.argv + ['--output-dir', str(OUTPUT_DIR)])
if also_set_environment: if also_set_environment:
monkeypatch.setenv('CIBW_OUTPUT_DIR', 'not_this_output_dir') monkeypatch.setenv('CIBW_OUTPUT_DIR', 'not_this_output_dir')
@@ -232,3 +233,21 @@ def test_build_selector_migrations(intercepted_build_args, monkeypatch, option_n
assert intercepted_build_selector.build_patterns == build_selector_patterns assert intercepted_build_selector.build_patterns == build_selector_patterns
else: else:
assert intercepted_build_selector.skip_patterns == build_selector_patterns assert intercepted_build_selector.skip_patterns == build_selector_patterns
@pytest.mark.parametrize('before_all', ["", None, 'test text'])
@pytest.mark.parametrize('platform_specific', [False, True])
def test_before_all(before_all, platform_specific, platform, intercepted_build_args, monkeypatch):
if before_all is not None:
if platform_specific:
monkeypatch.setenv('CIBW_BEFORE_ALL_' + platform.upper(), before_all)
monkeypatch.setenv('CIBW_BEFORE_ALL', 'overwritten')
else:
monkeypatch.setenv('CIBW_BEFORE_ALL', before_all)
main()
if before_all is None:
before_all = ""
assert intercepted_build_args.args[0].before_all == before_all