Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support qemu in GitHub Actions #482

Merged
merged 28 commits into from
Jan 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
76fe472
Support docker qemu emulation in any linux env
asfaltboy Dec 12, 2020
90a14b9
fixup! Support docker qemu emulation in any linux env
asfaltboy Dec 12, 2020
ec99c01
Allow architecture selection
asfaltboy Dec 16, 2020
0d29bf8
refactor: --archs, CIBW_ARCHS, "auto"
henryiii Dec 18, 2020
26b7b72
fix: drop printout
henryiii Dec 18, 2020
0c9af03
Add platform-specific CIBW_ARCHS options.
joerick Dec 20, 2020
f73b608
Add unit test for the option parsing
joerick Dec 21, 2020
7c86223
Add platform-specific arch option unit test
joerick Dec 21, 2020
5aa85e8
Add integration test for emulation
joerick Dec 21, 2020
0ca294e
Add separate runner for emulation tests
joerick Dec 21, 2020
91800e4
Update .github/workflows/test.yml
joerick Dec 21, 2020
27f1a4e
Use 'latest' for CI
joerick Dec 21, 2020
8a1a0a4
Add asserts on macOS/Windows so that they can't be anything but default
joerick Dec 21, 2020
b34bdee
Fix native archs expansion for windows
joerick Dec 21, 2020
eb03a11
Rename Architecture to match value amd64 -> AMD64
joerick Dec 23, 2020
3696f15
Add example github config
joerick Dec 23, 2020
68dc286
Add docs for ARCHS option
joerick Dec 23, 2020
3983fb2
Fix broken link in docs
joerick Dec 23, 2020
460eaa7
Add mini-guide to the FAQ section
joerick Dec 23, 2020
f6ce1c9
Update example config
joerick Dec 23, 2020
6b00931
Allow the testing of any of the example configs in /examples
joerick Dec 23, 2020
0893731
Simplify arch matching code
joerick Dec 28, 2020
b5f5437
Suggestion from review
joerick Dec 31, 2020
5ee663f
Add test for failure when setting CIBW_ARCHS on macos/windows
joerick Dec 31, 2020
f434072
option ordering
joerick Dec 31, 2020
d65da2e
docs edits from review
joerick Dec 31, 2020
5b5078e
Copy edit of --archs help text to make 'auto' clearer
joerick Dec 31, 2020
aa87128
Make the StrEnums proper Enums
joerick Dec 31, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-18.04, windows-latest, macos-latest]
os: [ubuntu-latest, windows-latest, macos-latest]
python_version: ['3.7']
timeout-minutes: 180
steps:
Expand Down Expand Up @@ -55,3 +55,24 @@ jobs:
- name: Test cibuildwheel
run: |
python ./bin/run_tests.py

test-emulated:
name: Test emulated cibuildwheel using qemu
runs-on: ubuntu-latest
timeout-minutes: 180
YannickJadoul marked this conversation as resolved.
Show resolved Hide resolved
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Install dependencies
run: |
python -m pip install -r requirements-dev.txt

- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
platforms: all

- name: Run the emulation tests
run: |
pytest --run-emulation test/test_emulation.py
54 changes: 39 additions & 15 deletions bin/run_example_ci_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import textwrap
import time
import click
from glob import glob
from collections import namedtuple
from urllib.parse import quote

Expand All @@ -30,49 +31,70 @@ def generate_basic_project(path):
project.generate(path)


CIService = namedtuple('CIService', 'name src_config_path dst_config_path badge_md')
CIService = namedtuple('CIService', 'name dst_config_path badge_md')
services = [
CIService(
name='appveyor',
src_config_path='examples/appveyor-minimal.yml',
dst_config_path='appveyor.yml',
badge_md='[![Build status](https://ci.appveyor.com/api/projects/status/wbsgxshp05tt1tif/branch/{branch}?svg=true)](https://ci.appveyor.com/project/joerick/cibuildwheel/branch/{branch})',
),
CIService(
name='azure-pipelines',
src_config_path='examples/azure-pipelines-minimal.yml',
dst_config_path='azure-pipelines.yml',
badge_md='[![Build Status](https://dev.azure.com/joerick0429/cibuildwheel/_apis/build/status/joerick.cibuildwheel?branchName={branch})](https://dev.azure.com/joerick0429/cibuildwheel/_build/latest?definitionId=2&branchName={branch})',
),
CIService(
name='circle-ci',
src_config_path='examples/circleci-minimal.yml',
name='circleci',
dst_config_path='.circleci/config.yml',
badge_md='[![CircleCI](https://circleci.com/gh/joerick/cibuildwheel/tree/{branch_escaped}.svg?style=svg)](https://circleci.com/gh/joerick/cibuildwheel/tree/{branch})',
),
CIService(
name='github',
src_config_path='examples/github-minimal.yml',
dst_config_path='.github/workflows/example.yml',
badge_md='[![Build](https://github.com/joerick/cibuildwheel/workflows/Build/badge.svg?branch={branch})](https://github.com/joerick/cibuildwheel/actions)',
),
CIService(
name='travis-ci',
src_config_path='examples/travis-ci-minimal.yml',
dst_config_path='.travis.yml',
badge_md='[![Build Status](https://travis-ci.org/joerick/cibuildwheel.svg?branch={branch})](https://travis-ci.org/joerick/cibuildwheel)',
),
CIService(
name='gitlab',
src_config_path='examples/gitlab-minimal.yml',
dst_config_path='.gitlab-ci.yml',
badge_md='[![Gitlab](https://gitlab.com/joerick/cibuildwheel/badges/{branch}/pipeline.svg)](https://gitlab.com/joerick/cibuildwheel/-/commits/{branch})'
),
]


def ci_service_for_config_file(config_file):
service_name = Path(config_file).name.rsplit('-', 1)[0]

for service in services:
if service.name == service_name:
return service

raise ValueError(f'unknown ci service for config file {config_file}')


@click.command()
def run_example_ci_configs():
@click.argument('config_files', nargs=-1, type=click.Path())
def run_example_ci_configs(config_files=None):
'''
Test the example configs. If no files are specified, will test
examples/*-minimal.yml
'''

if len(config_files) == 0:
config_files = glob('examples/*-minimal.yml')

# check each CI service has at most 1 config file
configs_by_service = {}
for config_file in config_files:
service = ci_service_for_config_file(config_file)
if service.name in configs_by_service:
raise Exception('You cannot specify more than one config per CI service')
configs_by_service[service.name] = config_file

if git_repo_has_changes():
print('Your git repo has uncommitted changes. Commit or stash before continuing.')
exit(1)
Expand All @@ -91,18 +113,19 @@ def run_example_ci_configs():
example_project = Path('example_root')
generate_basic_project(example_project)

for service in services:
src_config_file = Path(service.src_config_path)
for config_file in config_files:
service = ci_service_for_config_file(config_file)
src_config_file = Path(config_file)
dst_config_file = example_project / service.dst_config_path

dst_config_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(src_config_file, dst_config_file)

run(['git', 'add', example_project], check=True)
run(['git', 'commit', '-m', textwrap.dedent(f'''
run(['git', 'commit', '--no-verify', '-m', textwrap.dedent(f'''
Test example minimal configs

Testing files: {[s.src_config_path for s in services]}
Testing files: {config_files}
Generated from branch: {previous_branch}
Time: {timestamp}
''')], check=True)
Expand All @@ -116,9 +139,10 @@ def run_example_ci_configs():
print('> ')
print('> | Service | Config | Status |')
print('> |---|---|---|')
for service in services:
for config_file in config_files:
service = ci_service_for_config_file(config_file)
badge = service.badge_md.format(branch=branch_name, branch_escaped=quote(branch_name, safe=''))
print(f'> | {service.name} | `{service.src_config_path}` | {badge} |')
print(f'> | {service.name} | `{config_file}` | {badge} |')
print('> ')
print('> Generated by `bin/run_example_ci_config.py`')
print()
Expand Down
27 changes: 24 additions & 3 deletions cibuildwheel/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
parse_environment,
)
from cibuildwheel.util import (
Architecture,
BuildOptions,
BuildSelector,
DependencyConstraints,
Expand Down Expand Up @@ -67,6 +68,17 @@ def main() -> None:
run in Windows, and it will build and test for all versions of
Python. Default: auto.
''')

parser.add_argument('--archs',
default=None,
help='''
Comma-separated list of CPU architectures to build for.
When set to 'auto', builds the architectures natively supported
on this machine. Set this option to build an architecture
via emulation, for example, using binfmt_misc and QEMU.
Default: auto.
Choices: auto, {}
'''.format(", ".join(a.name for a in Architecture)))
parser.add_argument('--output-dir',
default=os.environ.get('CIBW_OUTPUT_DIR', 'wheelhouse'),
help='Destination folder for the wheels.')
Expand Down Expand Up @@ -168,8 +180,14 @@ def main() -> None:
print('cibuildwheel: Could not find setup.py, setup.cfg or pyproject.toml at root of package', file=sys.stderr)
exit(2)

if args.archs is not None:
archs_config_str = args.archs
else:
archs_config_str = get_option_from_environment('CIBW_ARCHS', platform=platform, default='auto')
archs = Architecture.parse_config(archs_config_str, platform=platform)

if args.print_build_identifiers:
print_build_identifiers(platform, build_selector)
print_build_identifiers(platform, build_selector, archs)
exit(0)

manylinux_images: Optional[Dict[str, str]] = None
Expand Down Expand Up @@ -202,6 +220,7 @@ def main() -> None:
manylinux_images[build_platform] = image

build_options = BuildOptions(
architectures=archs,
package_dir=package_dir,
output_dir=output_dir,
test_command=test_command,
Expand Down Expand Up @@ -284,10 +303,12 @@ def print_preamble(platform: str, build_options: BuildOptions) -> None:
print('\nHere we go!\n')


def print_build_identifiers(platform: str, build_selector: BuildSelector) -> None:
def print_build_identifiers(
platform: str, build_selector: BuildSelector, architectures: List[Architecture]
) -> None:
python_configurations: List[Any] = []
if platform == 'linux':
python_configurations = cibuildwheel.linux.get_python_configurations(build_selector)
python_configurations = cibuildwheel.linux.get_python_configurations(build_selector, architectures)
elif platform == 'windows':
python_configurations = cibuildwheel.windows.get_python_configurations(build_selector)
elif platform == 'macos':
Expand Down
44 changes: 16 additions & 28 deletions cibuildwheel/linux.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import platform
import subprocess
import sys
import textwrap
Expand All @@ -8,29 +7,10 @@

from .docker_container import DockerContainer
from .logger import log
from .util import (BuildOptions, BuildSelector, NonPlatformWheelError,
get_build_verbosity_extra_flags, prepare_command)


def matches_platform(identifier: str) -> bool:
pm = platform.machine()
if pm == "x86_64":
# x86_64 machines can run i686 docker containers
if identifier.endswith('x86_64') or identifier.endswith('i686'):
return True
elif pm == "i686":
if identifier.endswith('i686'):
return True
elif pm == "aarch64":
if identifier.endswith('aarch64'):
return True
elif pm == "ppc64le":
if identifier.endswith('ppc64le'):
return True
elif pm == "s390x":
if identifier.endswith('s390x'):
return True
return False
from .util import (
Architecture, BuildOptions, BuildSelector, NonPlatformWheelError,
get_build_verbosity_extra_flags, prepare_command,
)


class PythonConfiguration(NamedTuple):
Expand All @@ -43,7 +23,9 @@ def path(self):
return PurePath(self.path_str)


def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfiguration]:
def get_python_configurations(
build_selector: BuildSelector, architectures: List[Architecture]
) -> List[PythonConfiguration]:
python_configurations = [
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'),
Expand Down Expand Up @@ -78,8 +60,14 @@ def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfi
PythonConfiguration(version='3.8', identifier='cp38-manylinux_s390x', path_str='/opt/python/cp38-cp38'),
PythonConfiguration(version='3.9', identifier='cp39-manylinux_s390x', path_str='/opt/python/cp39-cp39'),
]
# skip builds as required
return [c for c in python_configurations if matches_platform(c.identifier) and build_selector(c.identifier)]

# return all configurations whose arch is in our `architectures` list,
# and match the build/skip rules
return [
c for c in python_configurations
if any(c.identifier.endswith(arch.value) for arch in architectures)
and build_selector(c.identifier)
]


def build(options: BuildOptions) -> None:
Expand All @@ -93,7 +81,7 @@ def build(options: BuildOptions) -> None:
exit(2)

assert options.manylinux_images is not None
python_configurations = get_python_configurations(options.build_selector)
python_configurations = get_python_configurations(options.build_selector, options.architectures)
joerick marked this conversation as resolved.
Show resolved Hide resolved
platforms = [
('cp', 'manylinux_x86_64', options.manylinux_images['x86_64']),
('cp', 'manylinux_i686', options.manylinux_images['i686']),
Expand Down
9 changes: 8 additions & 1 deletion cibuildwheel/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import subprocess
import sys
import tempfile
import textwrap
from os import PathLike
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Sequence, Union

from .environment import ParsedEnvironment
from .logger import log
from .util import (BuildOptions, BuildSelector, NonPlatformWheelError,
from .util import (Architecture, BuildOptions, BuildSelector, NonPlatformWheelError,
download, get_build_verbosity_extra_flags, get_pip_script,
install_certifi_script, prepare_command)

Expand Down Expand Up @@ -182,6 +183,12 @@ def setup_python(python_configuration: PythonConfiguration,


def build(options: BuildOptions) -> None:
if options.architectures != [Architecture.x86_64]:
raise ValueError(textwrap.dedent(f'''
Invalid archs option {options.architectures}. macOS only supports x86_64 for the moment.
If you want to set emulation architectures on Linux, use CIBW_ARCHS_LINUX instead.
joerick marked this conversation as resolved.
Show resolved Hide resolved
'''))

temp_dir = Path(tempfile.mkdtemp(prefix='cibuildwheel'))
built_wheel_dir = temp_dir / 'built_wheel'
repaired_wheel_dir = temp_dir / 'repaired_wheel'
Expand Down
39 changes: 38 additions & 1 deletion cibuildwheel/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import platform as platform_module
import re
import ssl
import textwrap
import urllib.request
Expand Down Expand Up @@ -121,10 +123,45 @@ def __repr__(self):
return f'{self.__class__.__name__}{self.base_file_path!r})'


class Architecture(Enum):
# mac/linux archs
x86_64 = 'x86_64'
i686 = 'i686'
aarch64 = 'aarch64'
ppc64le = 'ppc64le'
s390x = 's390x'

# windows archs
x86 = 'x86'
AMD64 = 'AMD64'

@staticmethod
def parse_config(config: str, platform: str) -> 'List[Architecture]':
result = []
for arch_str in re.split(r'[\s,]+', config):
if arch_str == 'auto':
result += Architecture.auto_archs(platform=platform)
else:
result.append(Architecture(arch_str))
joerick marked this conversation as resolved.
Show resolved Hide resolved
return result

@staticmethod
def auto_archs(platform: str) -> 'List[Architecture]':
joerick marked this conversation as resolved.
Show resolved Hide resolved
native_architecture = Architecture(platform_module.machine())
result = [native_architecture]
if platform == 'linux' and native_architecture == Architecture.x86_64:
# x86_64 machines can run i686 docker containers
result.append(Architecture.i686)
if platform == 'windows' and native_architecture == Architecture.AMD64:
result.append(Architecture.x86)
return result


class BuildOptions(NamedTuple):
package_dir: Path
output_dir: Path
build_selector: BuildSelector
architectures: List[Architecture]
environment: ParsedEnvironment
before_all: str
before_build: Optional[str]
Expand Down Expand Up @@ -164,7 +201,7 @@ def strtobool(val: str) -> bool:
return False


class CIProvider(str, Enum):
class CIProvider(Enum):
travis_ci = 'travis'
appveyor = 'appveyor'
circle_ci = 'circle_ci'
Expand Down
Loading