Compare commits

..
86 Commits
Author SHA1 Message Date
Joe Rickerby 5c4767899d Bump version: v1.5.5 2020-07-22 18:57:47 +01:00
Yannick Jadoul 3cd3a9dc77 Merge pull request #410 from YannickJadoul/fix-pwd-linux-docker
Fix cwd in Linux docker images
2020-07-22 14:41:16 +02:00
Yannick Jadoul 9e40094cfc Make timeout 2 hours on GitHub Actions and Azure Pipelines because of slow Windows jobs 2020-07-21 23:13:55 +02:00
Yannick Jadoul c91d669a90 Extend tests and add tests for CIBW_BEFORE_ALL 2020-07-21 20:12:28 +02:00
Yannick Jadoul f81c9474d6 Fix test_before_build test on Windows 2020-07-20 18:07:45 +02:00
Yannick Jadoul 2610f3220a Run Linux docker images with /project as cwd 2020-07-20 18:07:45 +02:00
Yannick Jadoul 29b0cdb08a Add cwd argument to DockerContainer 2020-07-20 18:07:45 +02:00
Yannick Jadoul 2b7da3fcbe Test whether CIBW_BEFORE_BUILD is executed from the right directory and confirm it is not 2020-07-20 18:07:45 +02:00
Joe Rickerby 726bcdf372 Merge pull request #411 from YannickJadoul/fix-docker-commandsubstitution
Fix command substitution in CIBW_ENVIRONMENT in Linux docker images
2020-07-20 16:38:30 +01:00
Yannick Jadoul 3142806f4b Capture output in DockerContainer.environment_executor to fix command substitutions in CIBW_ENVIRONMENT on Linux 2020-07-20 02:23:07 +02:00
Yannick Jadoul e28e43e7f9 Fix test_environment's checks on a command substitution in an environment 2020-07-20 02:02:50 +02:00
Joe Rickerby c0cb43800b Bump version: v1.5.4 2020-07-19 22:40:33 +01:00
Joe Rickerby cc4e8975c8 Merge pull request #408 from YannickJadoul/fix-bashlex_eval-quotes
Fix quotes in command in bashlex_eval
2020-07-19 19:47:05 +01:00
Joe Rickerby c5a9837784 Update cibuildwheel/bashlex_eval.py 2020-07-19 19:46:40 +01:00
Joe Rickerby ceecee0e68 Merge pull request #407 from samuelstjean/patch-1
fix typo in doc
2020-07-19 19:40:48 +01:00
Yannick Jadoul 28af49a006 Simplify by removing shlex.quote and shlex.split when using EnvironmentExecutor 2020-07-19 17:37:25 +02:00
Yannick Jadoul 3a2d7a21b1 Fix quotes-in-commandsubstitution problem with well-placed shlex.quote 2020-07-19 17:32:07 +02:00
Yannick Jadoul 44fd8b5c26 Fix test to demonstrate failure 2020-07-19 17:19:48 +02:00
Samuel St-Jean fddfaba0a4 fix typo 2020-07-19 10:32:27 -04:00
Joe Rickerby c884530a1d Bump version: v1.5.3 2020-07-19 11:59:56 +01:00
Joe Rickerby acf0320f68 Merge pull request #405 from mayeut/update-cp38
Update CPython 3.8 from 3.8.3 to 3.8.4
2020-07-18 11:52:57 +01:00
mayeut 5368b3bc64 Update CPython 3.8 from 3.8.3 to 3.8.4
Changelog: https://docs.python.org/release/3.8.4/whatsnew/changelog.html#changelog
2020-07-18 11:05:23 +02:00
Joe Rickerby acf60364c0 Merge pull request #403 from joerick/appveyor-debug-env
Debug Appveyor errors
2020-07-17 15:00:04 +01:00
Joe Rickerby 7d983cdf8c Reenable all tests 2020-07-17 09:39:22 +01:00
Joe Rickerby 6847454850 Remove debug code 2020-07-17 09:31:53 +01:00
Joe Rickerby af2a171994 Fix quotes 2020-07-17 09:09:02 +01:00
Joe Rickerby b90c4035b3 Update test_environment.py 2020-07-16 07:56:15 +01:00
Joe Rickerby 56cd6c965d Avoid backslash-in-env-var entirely 2020-07-15 22:02:20 +01:00
Joe Rickerby 5df97a4121 Correct quoting of semicolon 2020-07-15 21:34:30 +01:00
Joe Rickerby 69fe085ba3 more back-slash hilarity 2020-07-15 21:25:29 +01:00
Joe Rickerby 08eddeff8b Fix test and add another more awkward example 2020-07-15 21:07:33 +01:00
Joe Rickerby 9fb748bfb0 Add unit test for this failure mode 2020-07-12 21:45:46 +01:00
Joe Rickerby dc446d3cac moar debug 2020-07-12 12:45:59 +01:00
Joe Rickerby 9f71900637 add debugging code 2020-07-12 12:39:33 +01:00
Joe Rickerby e098e1450d Fix test project generation script 2020-07-12 12:35:30 +01:00
Joe Rickerby ed0077a995 Merge pull request #386 from joerick/docker-exec
From Linux bash script to Python control flow
2020-07-11 19:28:40 +01:00
Joe Rickerby 5842a75927 Flake8 fixes 2020-07-10 13:04:50 +01:00
Joe Rickerby 44e3198880 Convert glob to take a Path and a str pattern 2020-07-10 12:57:05 +01:00
Joe Rickerby 4c0af9f152 Finish file/dir tests 2020-07-10 12:46:32 +01:00
Joe Rickerby 8ddc6d57a2 Bump version: v1.5.2 2020-07-08 22:59:40 +01:00
Joe Rickerby 9e35d65de4 Add file-based unit tests 2020-07-08 22:56:33 +01:00
Joe Rickerby 58fa77b73b Merge pull request #401 from joerick/windows_call
Windows - distinguishing between call and shell for better arg quoting
2020-07-08 22:01:57 +01:00
Joe RickerbyandHenry Schreiner 048ddc4af3 Update test/test_pep518.py
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
2020-07-08 20:12:50 +01:00
Joe Rickerby c521dca40b My own unique attempt on the shell/call debacle 2020-07-08 18:17:55 +01:00
Henry Schreiner ac2b391790 Typo 2020-07-07 23:33:03 -04:00
Henry Schreiner 47fe71efb4 Try just converting to list 2020-07-07 23:09:40 -04:00
Henry Schreiner 53ba9bd0d0 Try adding PATH to env 2020-07-06 18:11:29 -04:00
Henry Schreiner 317397b39a fix: Try resolving 2020-07-06 14:25:02 -04:00
Henry Schreiner 8466194576 fix: Try removing shell 2020-07-06 10:49:24 -04:00
Henry Schreiner c68be58e78 fix: Try simple quoting 2020-07-06 08:55:36 -04:00
Henry Schreiner fc24230cb4 Try original 2020-07-06 01:31:47 -04:00
Henry Schreiner c36be04ee8 fix: Use repr 2020-07-05 23:51:28 -04:00
Henry Schreiner 7a27ae941c fix: Support environment markers in PEP 518 workaround 2020-07-05 23:16:00 -04:00
Joe Rickerby 833a069207 Pass PATH during command substitution test 2020-07-05 17:39:40 +01:00
Joe Rickerby cc913ee7cf Use a different method for the Windows test 2020-07-05 17:27:29 +01:00
Joe Rickerby d0b6401129 Merge pull request #394 from mayeut/update-cpython-3.7
Update CPython 3.7 from 3.7.7 to 3.7.8
2020-07-05 14:50:00 +01:00
mayeut 141b3e031e Update CPython 3.7 from 3.7.7 to 3.7.8
Changelog: https://docs.python.org/release/3.7.8/whatsnew/changelog.html#changelog
2020-07-04 18:11:06 +02:00
Yannick Jadoul 0beb9518c9 Merge pull request #393 from Czaki/fix-doc1
Update links in documentation
2020-07-04 16:37:53 +02:00
Grzegorz Bokota ad51e796c0 Update links in documentation
fix #391
2020-07-04 12:02:16 +02:00
Joe Rickerby b12a560e87 Add docker test images for other architectures 2020-07-01 13:22:24 +01:00
Joe Rickerby e31c24d3bb Fix overridden PATH test, by ensuring bash command substitution rules 2020-06-28 21:51:06 +01:00
Joe Rickerby 27d97daf13 Don't use redirection syntax in Windows environment. Fix absolute path use for Mac 2020-06-28 11:33:22 +01:00
Joe Rickerby faf6a76375 Change 'slow' tests to 'docker' tests, only run them on Linux 2020-06-28 10:53:03 +01:00
Joe Rickerby 91c3e90017 Fix Python 3.6 compatibility 2020-06-26 21:47:42 +01:00
Joe Rickerby ad871f886a Fix style 2020-06-26 21:41:00 +01:00
Joe Rickerby c956f118f6 Add unit tests for DockerContainer 2020-06-26 21:37:02 +01:00
Joe Rickerby 762dc807cd Convert DockerContainer to use binary PIPEs, to handle arbitrary data 2020-06-26 21:36:52 +01:00
Joe Rickerby 2edb33662b Import organisation 2020-06-26 20:22:49 +01:00
Joe Rickerby df69dda44a Fix broken PATH test - it was erroring for a different reason 2020-06-26 12:59:26 +01:00
Joe Rickerby 1d9333689f Explicit buffer size isn't necessary 2020-06-26 12:55:38 +01:00
Joe Rickerby 68d8f89fb3 Style tweaks 2020-06-26 11:02:23 +01:00
Joe Rickerby db9d658b51 Merge pull request #387 from joerick/call-types
Remove str() calls in call/shell functions using destructuring syntax
2020-06-26 00:20:22 +01:00
Joe Rickerby 1d88c27530 Neaten function call whitespace 2020-06-25 12:41:21 +01:00
Joe Rickerby fefe128a15 Fix issues with spaces in environment variables 2020-06-25 11:21:10 +01:00
Joe Rickerby a92959f191 Update README.md 2020-06-25 10:57:58 +01:00
Joe Rickerby 321335e756 Bump version: v1.5.1 2020-06-25 10:53:30 +01:00
Joe Rickerby c88a73b40b Merge pull request #389 from Czaki/fix_shutil
Switch back to shutil.move from Path.rename
2020-06-25 10:44:53 +01:00
Joe Rickerby dd4fc69e7c Revert "Remove hard-coded shell info to use the docker ENTRYPOINT instead"
This reverts commit a052c71aec.
2020-06-25 10:39:26 +01:00
Grzegorz Bokota 4dd5c0e11c use only dir name as target of shutil.move 2020-06-24 23:04:15 +00:00
Grzegorz Bokota a2f4d2aa47 Switch back to shutil.move from Path.rename 2020-06-24 22:36:18 +00:00
Joe Rickerby 850d109582 Don't use docker exec -w because CircleCI doesn't support it 2020-06-24 17:12:06 +01:00
Joe Rickerby a052c71aec Remove hard-coded shell info to use the docker ENTRYPOINT instead
This lets us use the 'linux32 bash' that's specified by the i686 docker
images, without specifying it explicitly.
2020-06-24 16:50:08 +01:00
Joe Rickerby 31339ba30e Extract DockerContainer into its own file 2020-06-24 16:43:33 +01:00
Joe Rickerby 53c00a0af6 Use a long-running remote bash shell, rather than docker exec 2020-06-24 16:39:20 +01:00
Joe Rickerby 50a263df21 Remove str() calls in call/shell functions using destructuring syntax 2020-06-23 20:28:54 +01:00
Joe Rickerby e417791ae4 Remove the linux bash script, replaced with a docker exec approach 2020-06-23 19:46:23 +01:00
31 changed files with 833 additions and 340 deletions
+1
View File
@@ -17,6 +17,7 @@ jobs:
matrix:
os: [ubuntu-18.04, windows-latest, macos-latest]
python_version: ['3.7']
timeout-minutes: 180
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
+40 -2
View File
@@ -79,7 +79,7 @@ env:
# Note: TWINE_PASSWORD is set to a PyPI API token in Travis settings
install:
- python3 -m pip install cibuildwheel==1.5.0
- python3 -m pip install cibuildwheel==1.5.5
script:
# build the wheels, put them into './wheelhouse'
@@ -94,7 +94,7 @@ after_success:
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
-------
@@ -152,6 +152,44 @@ This is similar to static linking, so it might have some licence implications. C
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_
+2
View File
@@ -21,6 +21,7 @@ jobs:
- job: windows_36
pool: {vmImage: 'vs2017-win2016'}
timeoutInMinutes: 180
steps:
- task: UsePythonVersion@0
inputs:
@@ -33,6 +34,7 @@ jobs:
- job: windows_38
pool: {vmImage: 'vs2017-win2016'}
timeoutInMinutes: 180
steps:
- task: UsePythonVersion@0
inputs:
+5 -1
View File
@@ -10,7 +10,11 @@ if __name__ == '__main__':
os.chdir(Path(__file__).resolve().parents[1])
# 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
subprocess.check_call([sys.executable, '-m', 'pytest', '-x', '--durations', '0', 'test'])
+1 -1
View File
@@ -1 +1 @@
__version__ = '1.5.0'
__version__ = '1.5.5'
+55 -25
View File
@@ -1,17 +1,23 @@
import shlex
import subprocess
from typing import Dict, NamedTuple
from typing import Callable, Dict, List, NamedTuple, Optional, Sequence
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):
environment: Dict[str, 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:
# empty string evaluates to empty string
# (but trips up bashlex)
@@ -26,7 +32,7 @@ def evaluate(value: str, environment: Dict[str, str]) -> str:
return evaluate_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':
return evaluate_word_node(node, context=context)
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':
return evaluate_parameter_node(node, context=context)
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:
word_start = node.pos[0]
word_end = node.pos[1]
word_string = context.input[word_start:word_end]
letters = list(word_string)
value = node.word
for part in node.parts:
part_start = part.pos[0] - word_start
part_end = part.pos[1] - word_start
part_string = context.input[part.pos[0]:part.pos[1]]
part_value = evaluate_node(part, context=context)
# Set all the characters in the part to None
for i in range(part_start, part_end):
letters[i] = ''
if part_string not in value:
raise RuntimeError(
'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
value = ''.join(letters)
# apply bash-like quotes/whitespace treatment
return ' '.join(word.strip() for word in shlex.split(value))
return value
def evaluate_command_node(node: bashlex.ast.node, context: NodeExecutionContext) -> str:
words = [evaluate_node(part, context=context) for part in node.parts]
command = ' '.join(words)
return subprocess.check_output(shlex.split(command), env=context.environment, universal_newlines=True)
if any(n.kind == 'operator' for n in node.parts):
return evaluate_nodes_as_compound_command(node.parts, context=context)
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:
+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))
+7 -5
View File
@@ -1,6 +1,6 @@
import bashlex # type: ignore
from typing import Dict, List, Mapping
from typing import Dict, List, Mapping, Optional
from . import bashlex_eval
@@ -46,9 +46,9 @@ class EnvironmentAssignment:
self.name = name
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'''
return bashlex_eval.evaluate(self.value, environment=environment)
return bashlex_eval.evaluate(self.value, environment=environment, executor=executor)
def as_shell_assignment(self) -> str:
return f'export {self.name}={self.value}'
@@ -61,11 +61,13 @@ class ParsedEnvironment:
def __init__(self, assignments: List[EnvironmentAssignment]):
self.assignments = assignments
def as_dictionary(self, prev_environment: Mapping[str, str]) -> Dict[str, str]:
def as_dictionary(self,
prev_environment: Mapping[str, str],
executor: Optional[bashlex_eval.EnvironmentExecutor] = None) -> Dict[str, str]:
environment = dict(**prev_environment)
for assignment in self.assignments:
value = assignment.evaluated_value(environment=environment)
value = assignment.evaluated_value(environment=environment, executor=executor)
environment[assignment.name] = value
return environment
+123 -203
View File
@@ -1,27 +1,14 @@
import os
import platform
import shlex
import subprocess
import sys
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 .util import (
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
)
from .docker_container import DockerContainer
from .util import (BuildOptions, BuildSelector,
get_build_verbosity_extra_flags, prepare_command)
def matches_platform(identifier: str) -> bool:
@@ -48,37 +35,41 @@ def matches_platform(identifier: str) -> bool:
class PythonConfiguration(NamedTuple):
version: 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]:
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='/opt/python/cp27-cp27mu'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_x86_64', path='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_x86_64', path='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_x86_64', path='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_x86_64', path='/opt/python/cp38-cp38'),
PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path='/opt/python/cp27-cp27m'),
PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path='/opt/python/cp27-cp27mu'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_i686', path='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_i686', path='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_i686', path='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_i686', path='/opt/python/cp38-cp38'),
PythonConfiguration(version='2.7', identifier='pp27-manylinux_x86_64', path='/opt/python/pp27-pypy_73'),
PythonConfiguration(version='3.6', identifier='pp36-manylinux_x86_64', path='/opt/python/pp36-pypy36_pp73'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_aarch64', path='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_aarch64', path='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_aarch64', path='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_aarch64', path='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_ppc64le', path='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_ppc64le', path='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_ppc64le', path='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_ppc64le', path='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_s390x', path='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_s390x', path='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_s390x', path='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_s390x', path='/opt/python/cp38-cp38'),
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_str='/opt/python/cp27-cp27mu'),
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_str='/opt/python/cp36-cp36m'),
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_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path_str='/opt/python/cp27-cp27m'),
PythonConfiguration(version='2.7', identifier='cp27-manylinux_i686', path_str='/opt/python/cp27-cp27mu'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_i686', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_i686', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_i686', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_i686', path_str='/opt/python/cp38-cp38'),
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_str='/opt/python/pp36-pypy36_pp73'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_aarch64', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_aarch64', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_aarch64', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_aarch64', path_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_ppc64le', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_ppc64le', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_ppc64le', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_ppc64le', path_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.5', identifier='cp35-manylinux_s390x', path_str='/opt/python/cp35-cp35m'),
PythonConfiguration(version='3.6', identifier='cp36-manylinux_s390x', path_str='/opt/python/cp36-cp36m'),
PythonConfiguration(version='3.7', identifier='cp37-manylinux_s390x', path_str='/opt/python/cp37-cp37m'),
PythonConfiguration(version='3.8', identifier='cp38-manylinux_s390x', path_str='/opt/python/cp38-cp38'),
]
# skip builds as required
return [c for c in python_configurations if matches_platform(c.identifier) and build_selector(c.identifier)]
@@ -111,201 +102,130 @@ def build(options: BuildOptions) -> None:
raise Exception('package_dir must be inside the working directory')
container_package_dir = PurePath('/project') / abs_package_dir.relative_to(cwd)
container_output_dir = PurePath('/output')
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)]
if not platform_configs:
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:
call(['docker', 'cp', '.', container_name + ':/project'])
with DockerContainer(docker_image, simulate_32_bit=platform_tag.endswith('i686'), cwd='/project') as docker:
docker.copy_into(Path.cwd(), PurePath('/project'))
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)
if options.before_all:
call(
['docker', 'exec', '-i', container_name] + shell_cmd,
universal_newlines=True,
input='''
PS4=' + '
before_all_prepared = prepare_command(options.before_all, project='/project', package=container_package_dir)
docker.call(['sh', '-c', before_all_prepared], env=env)
set -o errexit
set -o xtrace
for config in platform_configs:
dependency_constraint_flags: List[Union[str, PathLike]] = []
# add a modern Python interpreter to PATH so it can be used by BEFORE_ALL
# commands
export PATH=/opt/python/cp38-cp38:$PATH
if options.dependency_constraints:
constraints_file = options.dependency_constraints.get_for_python_version(config.version)
container_constraints_file = PurePath('/constraints.txt')
{environment_exports}
docker.copy_into(constraints_file, container_constraints_file)
dependency_constraint_flags = ['-c', container_constraints_file]
sh -c {before_all}
env = docker.get_environment()
'''.format(
environment_exports='\n'.join(options.environment.as_shell_commands()),
before_all=shlex.quote(prepare_command(options.before_all, project='/project', package=container_package_dir))
)
)
# put this config's python top of the list
python_bin = config.path / 'bin'
env['PATH'] = f'{python_bin}:{env["PATH"]}'
for config in platform_configs:
if options.dependency_constraints:
constraints_file = options.dependency_constraints.get_for_python_version(config.version)
env = options.environment.as_dictionary(env, executor=docker.environment_executor)
# `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.
with open(constraints_file, 'rb') as f:
call(
['docker', 'exec', '-i', container_name, 'sh', '-c', 'cat > /constraints.txt'],
input=f.read(),
)
# check config python and pip are still on PATH
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)
call(
['docker', 'exec', '-i', container_name] + shell_cmd,
universal_newlines=True,
input='''
# give xtrace output an extra level of indent inside docker
PS4=' + '
which_pip = docker.call(['which', 'pip'], env=env, capture_output=True).strip()
if PurePath(which_pip) != python_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)
exit(1)
set -o errexit
set -o xtrace
mkdir -p /output
cd /project
if options.before_build:
before_build_prepared = prepare_command(options.before_build, project='/project', package=container_package_dir)
docker.call(['sh', '-c', before_build_prepared], env=env)
PYBIN="{config_python_bin}"
temp_dir = PurePath('/tmp/cibuildwheel')
built_wheel_dir = temp_dir / 'built_wheel'
docker.call(['rm', '-rf', built_wheel_dir])
docker.call(['mkdir', '-p', built_wheel_dir])
export PATH="$PYBIN:$PATH"
{environment_exports}
docker.call([
'pip', 'wheel',
container_package_dir,
'-w', built_wheel_dir,
'--no-deps',
*get_build_verbosity_extra_flags(options.build_verbosity)
], env=env)
# check the active python and pip are in PYBIN
if [ "$(which pip)" != "$PYBIN/pip" ]; then
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."
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
built_wheel = docker.glob(built_wheel_dir, '*.whl')[0]
if [ ! -z {before_build} ]; then
sh -c {before_build}
fi
repaired_wheel_dir = temp_dir / 'repaired_wheel'
docker.call(['rm', '-rf', repaired_wheel_dir])
docker.call(['mkdir', '-p', repaired_wheel_dir])
# Build the wheel
rm -rf /tmp/built_wheel
mkdir /tmp/built_wheel
pip wheel {package_dir} -w /tmp/built_wheel --no-deps {build_verbosity_flag}
built_wheel=(/tmp/built_wheel/*.whl)
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)
# repair the wheel
rm -rf /tmp/repaired_wheels
mkdir /tmp/repaired_wheels
# NOTE: 'built_wheel' here is a bash array of glob matches; "$built_wheel" returns
# the first element
if [[ "$built_wheel" == *none-any.whl ]] || [ -z {repair_command} ]; then
# pure Python wheel or empty repair command
mv "$built_wheel" /tmp/repaired_wheels
else
sh -c {repair_command} repair_command "$built_wheel"
fi
repaired_wheels=(/tmp/repaired_wheels/*.whl)
repaired_wheels = docker.glob(repaired_wheel_dir, '*.whl')
if [ ! -z {test_command} ]; then
# Set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
pip install {dependency_install_flags} virtualenv
venv_dir=`mktemp -d`/venv
python -m virtualenv --no-download "$venv_dir"
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.
docker.call(['pip', 'install', 'virtualenv', *dependency_constraint_flags], env=env)
venv_dir = PurePath(docker.call(['mktemp', '-d'], capture_output=True).strip()) / 'venv'
# run the tests in a subshell to keep that `activate`
# script from polluting the env
(
source "$venv_dir/bin/activate"
docker.call(['python', '-m', 'virtualenv', '--no-download', venv_dir], env=env)
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
sh -c {before_test}
fi
if options.before_test:
before_test_prepared = prepare_command(options.before_test, project='/project', package=container_package_dir)
docker.call(['sh', '-c', before_test_prepared], env=virtualenv_env)
# Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel
# conforms to multiple manylinux standards. These multiple versions of the wheel are
# functionally the same, differing only in name, wheel metadata, and possibly include
# different external shared libraries. so it doesn't matter which one we run the tests on.
# Let's just pick the first one.
pip install "${{repaired_wheels[0]}}"{test_extras}
# Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel
# conforms to multiple manylinux standards. These multiple versions of the wheel are
# functionally the same, differing only in name, wheel metadata, and possibly include
# different external shared libraries. so it doesn't matter which one we run the tests on.
# Let's just pick the first one.
wheel_to_test = repaired_wheels[0]
docker.call(['pip', 'install', str(wheel_to_test) + options.test_extras], env=virtualenv_env)
# Install any requirements to run the tests
if [ ! -z "{test_requires}" ]; then
pip install {test_requires}
fi
# Install any requirements to run the tests
if options.test_requires:
docker.call(['pip', 'install', *options.test_requires], env=virtualenv_env)
# Run the tests from a different directory
pushd $HOME
sh -c {test_command}
popd
)
# exit if tests failed (needed for older bash versions)
if [ $? -ne 0 ]; then
exit 1;
fi
# Run the tests from a different directory
test_command_prepared = prepare_command(options.test_command, project='/project', package=container_package_dir)
docker.call(['sh', '-c', test_command_prepared], cwd='/root', env=virtualenv_env)
# clean up
rm -rf "$venv_dir"
fi
# clean up test environment
docker.call(['rm', '-rf', venv_dir])
# we're all done here; move it to output
mv "${{repaired_wheels[@]}}" /output
for repaired_wheel in "${{repaired_wheels[@]}}"; do
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 '',
)
)
# move repaired wheels to output
docker.call(['mkdir', '-p', container_output_dir])
docker.call(['mv', *repaired_wheels, container_output_dir])
# copy the output back into the host
call(['docker', 'cp',
container_name + ':/output/.',
str(options.output_dir.resolve())])
# copy the output back into the host
docker.copy_out(container_output_dir, options.output_dir)
except subprocess.CalledProcessError as error:
print(f'Command {error.cmd} failed with code {error.returncode}. {error.stdout}')
troubleshoot(options.package_dir, error)
exit(1)
finally:
# Still gets executed, even when 'exit(1)' gets called
call(['docker', 'rm', '--force', '-v', container_name])
def troubleshoot(package_dir: Path, error: Exception) -> None:
+30 -25
View File
@@ -4,27 +4,22 @@ import shutil
import subprocess
import sys
import tempfile
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 .util import (
BuildOptions,
BuildSelector,
download,
get_build_verbosity_extra_flags,
get_pip_script,
prepare_command,
)
from .util import (BuildOptions, BuildSelector, 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
if shell:
print(f'+ {args}')
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)
@@ -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='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.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.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.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.4/python-3.8.4-macosx10.9.pkg'),
# 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='3.6', identifier='pp36-macosx_x86_64', url='https://downloads.python.org/pypy/pypy3.6-v7.3.1-osx64.tar.bz2'),
@@ -103,7 +98,7 @@ def install_pypy(version: str, url: str) -> Path:
if not installation_path.exists():
downloaded_tar_bz2 = Path("/tmp") / pypy_tar_bz2
download(url, downloaded_tar_bz2)
call(['tar', '-C', '/tmp', '-xf', str(downloaded_tar_bz2)])
call(['tar', '-C', '/tmp', '-xf', downloaded_tar_bz2])
installation_bin_path = installation_path / 'bin'
python_executable = 'pypy3' if version[0] == '3' else 'pypy'
@@ -113,7 +108,9 @@ def install_pypy(version: str, url: str) -> 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'):
installation_bin_path = install_cpython(python_configuration.version, python_configuration.url)
elif python_configuration.identifier.startswith('pp'):
@@ -146,7 +143,7 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
exit(1)
# install pip & wheel
call(['python', str(get_pip_script)] + dependency_constraint_flags, env=env, cwd="/tmp")
call(['python', get_pip_script, *dependency_constraint_flags], env=env, cwd="/tmp")
assert (installation_bin_path / 'pip').exists()
call(['which', 'pip'], env=env)
call(['pip', '--version'], env=env)
@@ -154,7 +151,7 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
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)
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.
# CPython 3.5 defaults to 10.6, and pypy defaults to 10.7, causing
@@ -184,10 +181,10 @@ def build(options: BuildOptions) -> None:
python_configurations = get_python_configurations(options.build_selector)
for config in python_configurations:
dependency_constraint_flags = []
dependency_constraint_flags: Sequence[Union[str, PathLike]] = []
if options.dependency_constraints:
dependency_constraint_flags = [
'-c', str(options.dependency_constraints.get_for_python_version(config.version))
'-c', options.dependency_constraints.get_for_python_version(config.version)
]
env = setup_python(config, dependency_constraint_flags, options.environment)
@@ -201,9 +198,17 @@ def build(options: BuildOptions) -> None:
if built_wheel_dir.exists():
shutil.rmtree(built_wheel_dir)
built_wheel_dir.mkdir(parents=True)
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/joerick/cibuildwheel/pull/369
call(['pip', 'wheel', str(options.package_dir.resolve()), '-w', str(built_wheel_dir), '--no-deps'] + get_build_verbosity_extra_flags(options.build_verbosity), env=env)
call([
'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
@@ -212,7 +217,7 @@ def build(options: BuildOptions) -> None:
repaired_wheel_dir.mkdir(parents=True)
if built_wheel.name.endswith('none-any.whl') or not options.repair_command:
# pure Python wheel or empty repair command
built_wheel.rename(repaired_wheel_dir / built_wheel.name)
shutil.move(str(built_wheel), repaired_wheel_dir)
else:
repair_command_prepared = prepare_command(options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir)
call(repair_command_prepared, env=env, shell=True)
@@ -221,12 +226,12 @@ def build(options: BuildOptions) -> None:
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.
call(['pip', 'install', 'virtualenv'] + dependency_constraint_flags, env=env)
call(['pip', 'install', 'virtualenv', *dependency_constraint_flags], env=env)
venv_dir = Path(tempfile.mkdtemp())
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
call(['python', '-m', 'virtualenv', '--no-download', str(venv_dir)], env=env)
call(['python', '-m', 'virtualenv', '--no-download', venv_dir], env=env)
virtualenv_env = env.copy()
virtualenv_env['PATH'] = os.pathsep.join([
@@ -262,4 +267,4 @@ def build(options: BuildOptions) -> None:
shutil.rmtree(venv_dir)
# we're all done here; move it to output (overwrite existing)
repaired_wheel.replace(options.output_dir / repaired_wheel.name)
shutil.move(str(repaired_wheel), options.output_dir)
+51 -42
View File
@@ -3,30 +3,32 @@ import shutil
import subprocess
import sys
import tempfile
from os import PathLike
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Sequence, Union
from zipfile import ZipFile
import toml
from typing import Dict, List, Optional, NamedTuple
from .environment import ParsedEnvironment
from .util import (
BuildOptions,
BuildSelector,
download,
get_build_verbosity_extra_flags,
get_pip_script,
prepare_command,
)
from .util import (BuildOptions, BuildSelector, download,
get_build_verbosity_extra_flags, get_pip_script,
prepare_command)
IS_RUNNING_ON_AZURE = Path('C:\\hostedtoolcache').exists()
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:
print('+ ' + ' '.join(args))
return subprocess.check_call(' '.join(args), env=env, cwd=cwd, shell=True)
def call(args: Sequence[Union[str, PathLike]], env: Optional[Dict[str, str]] = None,
cwd: Optional[str] = None) -> None:
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]:
@@ -52,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.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.7.7', arch='32', identifier='cp37-win32', url=None),
PythonConfiguration(version='3.7.7', arch='64', identifier='cp37-win_amd64', url=None),
PythonConfiguration(version='3.8.3', arch='32', identifier='cp38-win32', url=None),
PythonConfiguration(version='3.8.3', arch='64', identifier='cp38-win_amd64', url=None),
PythonConfiguration(version='3.7.8', arch='32', identifier='cp37-win32', url=None),
PythonConfiguration(version='3.7.8', arch='64', identifier='cp37-win_amd64', url=None),
PythonConfiguration(version='3.8.4', arch='32', identifier='cp38-win32', url=None),
PythonConfiguration(version='3.8.4', arch='64', identifier='cp38-win_amd64', url=None),
# PyPy
PythonConfiguration(version='2.7', arch='32', identifier='pp27-win32', url='https://downloads.python.org/pypy/pypy2.7-v7.3.1-win32.zip'),
PythonConfiguration(version='3.6', arch='32', identifier='pp36-win32', url='https://downloads.python.org/pypy/pypy3.6-v7.3.1-win32.zip'),
@@ -80,7 +82,7 @@ def extract_zip(zip_src: Path, dest: Path) -> None:
def install_cpython(version: str, arch: str, nuget: Path) -> Path:
nuget_args = get_nuget_args(version, arch)
installation_path = Path(nuget_args[-1]) / (nuget_args[0] + '.' + version) / 'tools'
shell([str(nuget), 'install'] + nuget_args)
call([nuget, 'install', *nuget_args])
return installation_path
@@ -101,7 +103,7 @@ def install_pypy(version: str, arch: str, url: str) -> 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 = Path('C:\\cibw\\nuget.exe')
if not nuget.exists():
download('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', nuget)
@@ -129,9 +131,9 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
env = environment.as_dictionary(prev_environment=env)
# for the logs - check we're running the right version of python
shell(['where', 'python'], env=env)
shell(['python', '--version'], env=env)
shell(['python', '-c', '"import struct; print(struct.calcsize(\'P\') * 8)"'], env=env)
call(['where', 'python'], env=env)
call(['python', '--version'], 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()
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)
@@ -139,7 +141,7 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
# make sure pip is installed
if not (installation_path / 'Scripts' / 'pip.exe').exists():
shell(['python', str(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 (installation_path / 'Scripts' / 'pip.exe').exists()
where_pip = subprocess.check_output(['where', 'pip'], env=env, universal_newlines=True).splitlines()[0].strip()
if where_pip.strip() != str(installation_path / 'Scripts' / 'pip.exe'):
@@ -147,9 +149,9 @@ def setup_python(python_configuration: PythonConfiguration, dependency_constrain
exit(1)
# prepare the Python environment
shell(['python', '-m', 'pip', 'install', '--upgrade', 'pip'] + dependency_constraint_flags, env=env)
shell(['pip', '--version'], env=env)
shell(['pip', 'install', '--upgrade', 'setuptools', 'wheel'] + dependency_constraint_flags, env=env)
call(['python', '-m', 'pip', 'install', '--upgrade', 'pip', *dependency_constraint_flags], env=env)
call(['pip', '--version'], env=env)
call(['pip', 'install', '--upgrade', 'setuptools', 'wheel', *dependency_constraint_flags], env=env)
return env
@@ -191,14 +193,14 @@ def build(options: BuildOptions) -> None:
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)
shell(before_all_prepared, env=env)
python_configurations = get_python_configurations(options.build_selector)
for config in python_configurations:
dependency_constraint_flags = []
dependency_constraint_flags: Sequence[Union[str, PathLike]] = []
if options.dependency_constraints:
dependency_constraint_flags = [
'-c', str(options.dependency_constraints.get_for_python_version(config.version))
'-c', options.dependency_constraints.get_for_python_version(config.version)
]
# install Python
@@ -207,7 +209,7 @@ def build(options: BuildOptions) -> None:
# run the before_build command
if options.before_build:
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):
@@ -220,7 +222,14 @@ def build(options: BuildOptions) -> None:
built_wheel_dir.mkdir(parents=True)
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/joerick/cibuildwheel/pull/369
shell(['pip', 'wheel', str(options.package_dir.resolve()), '-w', str(built_wheel_dir), '--no-deps'] + get_build_verbosity_extra_flags(options.build_verbosity), env=env)
call([
'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
@@ -229,21 +238,21 @@ def build(options: BuildOptions) -> None:
repaired_wheel_dir.mkdir(parents=True)
if built_wheel.name.endswith('none-any.whl') or not options.repair_command:
# pure Python wheel or empty repair command
built_wheel.rename(repaired_wheel_dir / built_wheel.name)
shutil.move(str(built_wheel), repaired_wheel_dir)
else:
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 = next(repaired_wheel_dir.glob('*.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.
shell(['pip', 'install', 'virtualenv'] + dependency_constraint_flags, env=env)
call(['pip', 'install', 'virtualenv', *dependency_constraint_flags], env=env)
venv_dir = Path(tempfile.mkdtemp())
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
shell(['python', '-m', 'virtualenv', '--no-download', str(venv_dir)], env=env)
call(['python', '-m', 'virtualenv', '--no-download', venv_dir], env=env)
virtualenv_env = env.copy()
virtualenv_env['PATH'] = os.pathsep.join([
@@ -252,7 +261,7 @@ def build(options: BuildOptions) -> None:
])
# 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:
before_test_prepared = prepare_command(
@@ -260,14 +269,14 @@ def build(options: BuildOptions) -> None:
project='.',
package=options.package_dir
)
shell([before_test_prepared], env=virtualenv_env)
shell(before_test_prepared, env=virtualenv_env)
# install the wheel
shell(['pip', 'install', str(repaired_wheel) + options.test_extras], env=virtualenv_env)
call(['pip', 'install', str(repaired_wheel) + options.test_extras], env=virtualenv_env)
# test the wheel
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
# (this ensures that Python runs the tests against the installed wheel
@@ -277,10 +286,10 @@ def build(options: BuildOptions) -> None:
project=Path('.').resolve(),
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
shutil.rmtree(venv_dir)
# we're all done here; move it to output (remove if already exists)
repaired_wheel.replace(options.output_dir / repaired_wheel.name)
shutil.move(str(repaired_wheel), options.output_dir)
+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.
[`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 -1
View File
@@ -12,7 +12,7 @@ stack: python 3.7
init:
- cmd: set PATH=C:\Python37;C:\Python37\Scripts;%PATH%
install: python -m pip install cibuildwheel==1.5.0
install: python -m pip install cibuildwheel==1.5.5
build_script: python -m cibuildwheel --output-dir wheelhouse
+3 -3
View File
@@ -5,7 +5,7 @@ jobs:
- task: UsePythonVersion@0
- bash: |
python3 -m pip install --upgrade pip
pip3 install cibuildwheel==1.5.0
pip3 install cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse .
- task: PublishBuildArtifacts@1
inputs: {pathtoPublish: 'wheelhouse'}
@@ -16,7 +16,7 @@ jobs:
- task: UsePythonVersion@0
- bash: |
python3 -m pip install --upgrade pip
pip3 install cibuildwheel==1.5.0
pip3 install cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse .
- task: PublishBuildArtifacts@1
inputs: {pathtoPublish: 'wheelhouse'}
@@ -29,7 +29,7 @@ jobs:
displayName: Install Visual C++ for Python 2.7
- bash: |
python -m pip install --upgrade pip
pip install cibuildwheel==1.5.0
pip install cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse .
- task: PublishBuildArtifacts@1
inputs: {pathtoPublish: 'wheelhouse'}
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
- run:
name: Build the Linux wheels.
command: |
pip3 install --user cibuildwheel==1.5.0
pip3 install --user cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse
- store_artifacts:
path: wheelhouse/
@@ -25,7 +25,7 @@ jobs:
- run:
name: Build the OS X wheels.
command: |
pip3 install --user cibuildwheel==1.5.0
pip3 install --user cibuildwheel==1.5.5
cibuildwheel --output-dir wheelhouse
- store_artifacts:
path: wheelhouse/
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Install cibuildwheel
run: |
python -m pip install cibuildwheel==1.5.0
python -m pip install cibuildwheel==1.5.5
- name: Install Visual C++ for Python 2.7
if: runner.os == 'Windows'
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Install cibuildwheel
run: |
python -m pip install cibuildwheel==1.5.0
python -m pip install cibuildwheel==1.5.5
- name: Install Visual C++ for Python 2.7
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
install:
- python3 -m pip install cibuildwheel==1.5.0
- python3 -m pip install cibuildwheel==1.5.5
script:
# 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
install:
- python3 -m pip install cibuildwheel==1.5.0
- python3 -m pip install cibuildwheel==1.5.5
script:
# build the wheels, put them into './wheelhouse'
+3 -3
View File
@@ -55,7 +55,7 @@ jobs:
- stage: deploy
name: Build and deploy Linux wheels
services: docker
install: python3 -m pip install cibuildwheel==1.5.0
install: python3 -m pip install cibuildwheel==1.5.5
script: python3 -m cibuildwheel --output-dir wheelhouse
after_success: |
python3 -m pip install twine
@@ -65,7 +65,7 @@ jobs:
name: Build and deploy macOS wheels
os: osx
language: shell
install: python3 -m pip install cibuildwheel==1.5.0
install: python3 -m pip install cibuildwheel==1.5.5
script: python3 -m cibuildwheel --output-dir wheelhouse
after_success: |
python3 -m pip install twine
@@ -75,7 +75,7 @@ jobs:
name: Build and deploy Windows wheels
os: windows
language: shell
install: python3 -m pip install cibuildwheel==1.5.0
install: python3 -m pip install cibuildwheel==1.5.5
script: python3 -m cibuildwheel --output-dir wheelhouse
after_success: |
python3 -m pip install twine
+2
View File
@@ -6,3 +6,5 @@ pymdown-extensions
pip-tools
requests
click
mypy
typing-extensions
+1 -1
View File
@@ -1,5 +1,5 @@
[flake8]
ignore = E501,W503,E741
ignore = E501,W503,E741,E226
application-import-names = cibuildwheel
exclude =
cibuildwheel/resources/,
+1 -1
View File
@@ -11,7 +11,7 @@ long_description = (this_directory / 'README.md').read_text(encoding='utf-8')
setup(
name='cibuildwheel',
version='1.5.0',
version='1.5.5',
install_requires=['bashlex!=0.13', 'toml'],
description="Build Python wheels on CI with minimal configuration.",
long_description=long_description,
+28 -3
View File
@@ -1,4 +1,5 @@
import os
import pytest
import subprocess
import textwrap
from . import utils
@@ -21,7 +22,7 @@ def test(tmp_path):
project_dir = tmp_path / 'project'
project_with_before_build_asserts.generate(project_dir)
with open(os.path.join(project_dir, "text_info.txt"), mode='w') as ff:
with (project_dir / 'text_info.txt').open(mode='w') as ff:
print("dummy text", file=ff)
# build the wheels
@@ -33,6 +34,30 @@ def test(tmp_path):
})
# also check that we got the right wheels
os.remove(os.path.join(project_dir, "text_info.txt"))
(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
from . import utils
@@ -32,14 +34,41 @@ def test(tmp_path):
project_dir = tmp_path / 'project'
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
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_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_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': before_build.format(output_dir='/tmp/'),
'CIBW_BEFORE_BUILD_WINDOWS': before_build.format(output_dir=r'c:\\'),
})
# also check that we got the right wheels
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_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)
+21 -5
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
CIBW_TEST_VAR = os.environ.get("CIBW_TEST_VAR")
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")
if CIBW_TEST_VAR != "a b c":
raise Exception('CIBW_TEST_VAR should equal "a b c". It was "%s"' % CIBW_TEST_VAR)
if CIBW_TEST_VAR_2 != "1":
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:
raise Exception('PATH should contain "/opt/cibw_test_path". It was "%s"' % PATH)
if "$PATH" in PATH:
@@ -44,7 +47,7 @@ def test(tmp_path):
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'
output_dir = tmp_path / 'output'
@@ -54,8 +57,21 @@ def test_overridden_path(tmp_path):
# mess up PATH, somehow
with pytest.raises(subprocess.CalledProcessError):
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_ENVIRONMENT_WINDOWS': '''SOMETHING="$(mkdir new_path && type nul > new_path/python.exe)" PATH="$CD\\new_path;$PATH"''',
})
if utils.platform == 'linux':
utils.cibuildwheel_run(project_dir, output_dir=output_dir, add_env={
'CIBW_BEFORE_ALL': 'mkdir new_path && touch new_path/python && chmod +x new_path/python',
'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
captured = capfd.readouterr()
assert "python available on PATH doesn't match our installed instance" in captured.err
+8 -3
View File
@@ -6,8 +6,12 @@ basic_project = test_projects.new_c_project(
setup_py_add=textwrap.dedent(
"""
# Will fail if PEP 518 does work
import sys
import requests
assert requests.__version__ == "2.23.0", "Requests found but wrong version ({0})".format(requests.__version__)
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
@@ -22,9 +26,10 @@ basic_project.files[
] = """
[build-system]
requires = [
"setuptools>=42",
"setuptools >= 42",
"wheel",
"requests==2.23.0"
"requests==2.22.0; python_version<'3.6'",
"requests==2.23.0; python_version>='3.6'"
]
build-backend = "setuptools.build_meta"
+5 -4
View File
@@ -1,8 +1,9 @@
from argparse import ArgumentParser
import importlib
import tempfile
import sys
import subprocess
import sys
import tempfile
from argparse import ArgumentParser
from pathlib import Path
def main():
@@ -19,7 +20,7 @@ def main():
project = getattr(importlib.import_module(module), name)
project_dir = tempfile.mkdtemp()
project_dir = Path(tempfile.mkdtemp())
project.generate(project_dir)
print('Project generated at', project_dir)
+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)
+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"
+32 -3
View File
@@ -40,7 +40,7 @@ def test_inheritance():
def test_shell_eval():
environment_recipe = parse_environment('VAR="$(echo "a test" string)"')
environment_recipe = parse_environment('VAR="$(echo "a test" string)"')
env_copy = os.environ.copy()
env_copy.pop('VAR', None)
@@ -50,8 +50,8 @@ def test_shell_eval():
)
environment_cmds = environment_recipe.as_shell_commands()
assert environment_dict['VAR'] == 'a test string'
assert environment_cmds == ['export VAR="$(echo "a test" string)"']
assert environment_dict['VAR'] == 'a test string'
assert environment_cmds == ['export VAR="$(echo "a test" string)"']
def test_shell_eval_and_env():
@@ -94,3 +94,32 @@ def test_no_vars_pass_through():
environment_dict = environment_recipe.as_dictionary(prev_environment={'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!'