Files
cibuildwheel/cibuildwheel/util.py
T

231 lines
7.0 KiB
Python
Raw Normal View History

2019-11-12 23:51:27 +00:00
import os
2020-12-20 19:42:28 +00:00
import platform as platform_module
import re
import ssl
2020-11-23 21:00:22 +00:00
import textwrap
import urllib.request
from enum import Enum
2017-04-11 22:57:42 +01:00
from fnmatch import fnmatch
2020-06-15 01:53:31 +02:00
from pathlib import Path
from time import sleep
2020-06-15 01:53:31 +02:00
from typing import Dict, List, NamedTuple, Optional, Union
2020-11-23 21:00:22 +00:00
import certifi
from .environment import ParsedEnvironment
2020-06-15 01:53:31 +02:00
def prepare_command(command: str, **kwargs: Union[str, os.PathLike]) -> str:
'''
2019-11-12 23:34:59 +00:00
Preprocesses a command by expanding variables like {python}.
2019-11-12 23:34:59 +00:00
For example, used in the test_command option to specify the path to the
project's root.
'''
2019-11-12 23:34:59 +00:00
return command.format(python='python', pip='pip', **kwargs)
2017-04-11 22:57:42 +01:00
def get_build_verbosity_extra_flags(level: int) -> List[str]:
if level > 0:
return ['-' + level * 'v']
elif level < 0:
return ['-' + -level * 'q']
else:
return []
2020-02-03 20:42:55 +01:00
class BuildSelector:
def __init__(self, build_config: str, skip_config: str):
self.build_patterns = build_config.split()
self.skip_patterns = skip_config.split()
2017-04-11 22:57:42 +01:00
def __call__(self, build_id: str) -> bool:
def match_any(patterns: List[str]) -> bool:
return any(fnmatch(build_id, pattern) for pattern in patterns)
return match_any(self.build_patterns) and not match_any(self.skip_patterns)
2017-04-13 15:02:04 +01:00
def __repr__(self) -> str:
2020-11-01 21:17:25 +00:00
if not self.skip_patterns:
return f'BuildSelector({" ".join(self.build_patterns)!r})'
else:
return f'BuildSelector({" ".join(self.build_patterns)!r} - {" ".join(self.skip_patterns)!r})'
# Taken from https://stackoverflow.com/a/107717
2020-02-03 20:42:55 +01:00
class Unbuffered:
def __init__(self, stream): # type: ignore
self.stream = stream
def write(self, data): # type: ignore
self.stream.write(data)
self.stream.flush()
def writelines(self, datas): # type: ignore
self.stream.writelines(datas)
self.stream.flush()
def __getattr__(self, attr): # type: ignore
return getattr(self.stream, attr)
2020-06-15 01:53:31 +02:00
def download(url: str, dest: Path) -> None:
print(f'+ Download {url} to {dest}')
dest_dir = dest.parent
if not dest_dir.exists():
dest_dir.mkdir(parents=True)
# we've had issues when relying on the host OS' CA certificates on Windows,
# so we use certifi (this sounds odd but requests also does this by default)
cafile = os.environ.get('SSL_CERT_FILE', certifi.where())
context = ssl.create_default_context(cafile=cafile)
repeat_num = 3
for i in range(repeat_num):
try:
response = urllib.request.urlopen(url, context=context)
2019-11-12 23:51:27 +00:00
except Exception:
if i == repeat_num - 1:
raise
sleep(3)
continue
break
try:
2020-06-15 01:53:31 +02:00
dest.write_bytes(response.read())
finally:
response.close()
class DependencyConstraints:
2020-06-15 01:53:31 +02:00
def __init__(self, base_file_path: Path):
assert base_file_path.exists()
self.base_file_path = base_file_path.resolve()
2020-04-10 20:24:52 +02:00
@staticmethod
def with_defaults() -> 'DependencyConstraints':
return DependencyConstraints(
2020-06-17 00:18:40 +02:00
base_file_path=resources_dir / 'constraints.txt'
)
2020-06-15 01:53:31 +02:00
def get_for_python_version(self, version: str) -> Path:
version_parts = version.split('.')
# try to find a version-specific dependency file e.g. if
# ./constraints.txt is the base, look for ./constraints-python27.txt
2020-06-15 01:53:31 +02:00
specific_stem = self.base_file_path.stem + f'-python{version_parts[0]}{version_parts[1]}'
2020-06-17 00:18:40 +02:00
specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name)
2020-06-15 01:53:31 +02:00
if specific_file_path.exists():
return specific_file_path
else:
return self.base_file_path
2020-11-01 21:17:25 +00:00
def __repr__(self):
2020-11-13 15:38:34 +00:00
return f'{self.__class__.__name__}{self.base_file_path!r})'
2020-12-16 23:17:51 +00:00
class Architecture(str, Enum):
2020-12-21 11:05:19 +00:00
# mac/linux archs
2020-12-16 23:17:51 +00:00
x86_64 = 'x86_64'
i686 = 'i686'
aarch64 = 'aarch64'
ppc64le = 'ppc64le'
s390x = 's390x'
2020-12-21 11:05:19 +00:00
# windows archs
x86 = 'x86'
AMD64 = 'AMD64'
2020-12-20 19:42:28 +00:00
@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))
return result
@staticmethod
def auto_archs(platform: str) -> 'List[Architecture]':
native_architecture = Architecture(platform_module.machine())
result = [native_architecture]
if platform == 'linux' and native_architecture == Architecture.x86_64:
2020-12-21 16:34:36 +00:00
# x86_64 machines can run i686 docker containers
2020-12-20 19:42:28 +00:00
result.append(Architecture.i686)
if platform == 'windows' and native_architecture == Architecture.AMD64:
2020-12-21 16:34:36 +00:00
result.append(Architecture.x86)
2020-12-20 19:42:28 +00:00
return result
2020-12-16 23:17:51 +00:00
class BuildOptions(NamedTuple):
2020-06-15 01:53:31 +02:00
package_dir: Path
output_dir: Path
build_selector: BuildSelector
2020-12-31 15:22:19 +00:00
architectures: List[Architecture]
environment: ParsedEnvironment
2020-05-11 17:09:54 +02:00
before_all: str
before_build: Optional[str]
repair_command: str
manylinux_images: Optional[Dict[str, str]]
dependency_constraints: Optional[DependencyConstraints]
test_command: Optional[str]
before_test: Optional[str]
test_requires: List[str]
test_extras: str
2020-05-11 18:04:09 +02:00
build_verbosity: int
2020-04-06 22:38:03 +02:00
2020-06-15 01:53:31 +02:00
resources_dir = Path(__file__).resolve().parent / 'resources'
get_pip_script = resources_dir / 'get-pip.py'
2020-10-10 18:24:15 +02:00
install_certifi_script = resources_dir / "install_certifi.py"
class NonPlatformWheelError(Exception):
def __init__(self) -> None:
message = textwrap.dedent('''
cibuildwheel: Build failed because a pure Python wheel was generated.
If you intend to build a pure-Python wheel, you don't need cibuildwheel - use
`pip wheel -w DEST_DIR .` instead.
If you expected a platform wheel, check your project configuration, or run
cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
''')
super().__init__(message)
2020-11-23 21:00:22 +00:00
def strtobool(val: str) -> bool:
if val.lower() in ('y', 'yes', 't', 'true', 'on', '1'):
return True
return False
class CIProvider(str, Enum):
travis_ci = 'travis'
appveyor = 'appveyor'
circle_ci = 'circle_ci'
azure_pipelines = 'azure_pipelines'
github_actions = 'github_actions'
gitlab = 'gitlab'
other = 'other'
def detect_ci_provider() -> Optional[CIProvider]:
if 'TRAVIS' in os.environ:
return CIProvider.travis_ci
elif 'APPVEYOR' in os.environ:
return CIProvider.appveyor
elif 'CIRCLECI' in os.environ:
return CIProvider.circle_ci
elif 'AZURE_HTTP_USER_AGENT' in os.environ:
return CIProvider.azure_pipelines
elif 'GITHUB_ACTIONS' in os.environ:
return CIProvider.github_actions
elif 'GITLAB_CI' in os.environ:
return CIProvider.gitlab
elif strtobool(os.environ.get('CI', 'false')):
return CIProvider.other
else:
return None