feature: allow local runs on windows/macOS (#974)
* feature: allow local runs on windows/macOS Cache python installations to a user cache folder using platformdirs. The build environment is now a virtual environment to allow proper isolation. Allows to run tests in parallel. Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
co-authored by
Joe Rickerby
parent
81243337fd
commit
ab13f7a9de
@@ -59,8 +59,6 @@ Usage
|
|||||||
|
|
||||||
<sup>¹ [Requires emulation](https://cibuildwheel.readthedocs.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.</sup><br>
|
<sup>¹ [Requires emulation](https://cibuildwheel.readthedocs.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.</sup><br>
|
||||||
|
|
||||||
`cibuildwheel` is not intended to run on your development machine. Because it uses system Python from Python.org on macOS and Windows, it will try to install packages globally - not what you expect from a build tool! Instead, isolated CI services like those mentioned above are ideal. For Linux builds, it uses manylinux docker images, so those can be done locally for testing in a pinch.
|
|
||||||
|
|
||||||
<!--intro-end-->
|
<!--intro-end-->
|
||||||
|
|
||||||
Example setup
|
Example setup
|
||||||
|
|||||||
+1
-3
@@ -16,15 +16,13 @@ if __name__ == "__main__":
|
|||||||
unit_test_args += ["--run-docker"]
|
unit_test_args += ["--run-docker"]
|
||||||
subprocess.run(unit_test_args, check=True)
|
subprocess.run(unit_test_args, check=True)
|
||||||
|
|
||||||
xdist_test_args = ["-n", "2"] if sys.platform.startswith("linux") else []
|
|
||||||
|
|
||||||
# run the integration tests
|
# run the integration tests
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[
|
[
|
||||||
sys.executable,
|
sys.executable,
|
||||||
"-m",
|
"-m",
|
||||||
"pytest",
|
"pytest",
|
||||||
*xdist_test_args,
|
"--numprocesses=2",
|
||||||
"-x",
|
"-x",
|
||||||
"--durations",
|
"--durations",
|
||||||
"0",
|
"0",
|
||||||
|
|||||||
Executable
+120
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
import click
|
||||||
|
import rich
|
||||||
|
import tomli
|
||||||
|
from packaging.version import InvalidVersion, Version
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
from rich.syntax import Syntax
|
||||||
|
|
||||||
|
from cibuildwheel.typing import Final
|
||||||
|
|
||||||
|
log = logging.getLogger("cibw")
|
||||||
|
|
||||||
|
# Looking up the dir instead of using utils.resources_dir
|
||||||
|
# since we want to write to it.
|
||||||
|
DIR: Final[Path] = Path(__file__).parent.parent.resolve()
|
||||||
|
RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"
|
||||||
|
|
||||||
|
GET_VIRTUALENV_GITHUB: Final[str] = "https://github.com/pypa/get-virtualenv"
|
||||||
|
GET_VIRTUALENV_URL_TEMPLATE: Final[
|
||||||
|
str
|
||||||
|
] = f"{GET_VIRTUALENV_GITHUB}/blob/{{version}}/public/virtualenv.pyz?raw=true"
|
||||||
|
|
||||||
|
|
||||||
|
class VersionTuple(NamedTuple):
|
||||||
|
version: Version
|
||||||
|
version_string: str
|
||||||
|
|
||||||
|
|
||||||
|
def git_ls_remote_versions(url) -> list[VersionTuple]:
|
||||||
|
versions: list[VersionTuple] = []
|
||||||
|
tags = subprocess.run(
|
||||||
|
["git", "ls-remote", "--tags", url], check=True, text=True, capture_output=True
|
||||||
|
).stdout.splitlines()
|
||||||
|
for tag in tags:
|
||||||
|
_, ref = tag.split()
|
||||||
|
assert ref.startswith("refs/tags/")
|
||||||
|
version_string = ref[10:]
|
||||||
|
try:
|
||||||
|
version = Version(version_string)
|
||||||
|
if version.is_devrelease:
|
||||||
|
log.info(f"Ignoring development release '{version}'")
|
||||||
|
continue
|
||||||
|
if version.is_prerelease:
|
||||||
|
log.info(f"Ignoring pre-release '{version}'")
|
||||||
|
continue
|
||||||
|
versions.append(VersionTuple(version, version_string))
|
||||||
|
except InvalidVersion:
|
||||||
|
log.warning(f"Ignoring ref '{ref}'")
|
||||||
|
versions.sort(reverse=True)
|
||||||
|
return versions
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option("--force", is_flag=True)
|
||||||
|
@click.option(
|
||||||
|
"--level", default="INFO", type=click.Choice(["WARNING", "INFO", "DEBUG"], case_sensitive=False)
|
||||||
|
)
|
||||||
|
def update_virtualenv(force: bool, level: str) -> None:
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level="INFO",
|
||||||
|
format="%(message)s",
|
||||||
|
datefmt="[%X]",
|
||||||
|
handlers=[RichHandler(rich_tracebacks=True, markup=True)],
|
||||||
|
)
|
||||||
|
log.setLevel(level)
|
||||||
|
|
||||||
|
toml_file_path = RESOURCES_DIR / "virtualenv.toml"
|
||||||
|
|
||||||
|
original_toml = toml_file_path.read_text()
|
||||||
|
with toml_file_path.open("rb") as f:
|
||||||
|
loaded_file = tomli.load(f)
|
||||||
|
version = str(loaded_file["version"])
|
||||||
|
versions = git_ls_remote_versions(GET_VIRTUALENV_GITHUB)
|
||||||
|
if versions[0].version > Version(version):
|
||||||
|
version = versions[0].version_string
|
||||||
|
|
||||||
|
result_toml = (
|
||||||
|
f'version = "{version}"\n'
|
||||||
|
f'url = "{GET_VIRTUALENV_URL_TEMPLATE.format(version=version)}"\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
rich.print() # spacer
|
||||||
|
|
||||||
|
if original_toml == result_toml:
|
||||||
|
rich.print("[green]Check complete, virtualenv version unchanged.")
|
||||||
|
return
|
||||||
|
|
||||||
|
rich.print("virtualenv version updated.")
|
||||||
|
rich.print("Changes:")
|
||||||
|
rich.print()
|
||||||
|
|
||||||
|
toml_relpath = toml_file_path.relative_to(DIR).as_posix()
|
||||||
|
diff_lines = difflib.unified_diff(
|
||||||
|
original_toml.splitlines(keepends=True),
|
||||||
|
result_toml.splitlines(keepends=True),
|
||||||
|
fromfile=toml_relpath,
|
||||||
|
tofile=toml_relpath,
|
||||||
|
)
|
||||||
|
rich.print(Syntax("".join(diff_lines), "diff", theme="ansi_light"))
|
||||||
|
rich.print()
|
||||||
|
|
||||||
|
if force:
|
||||||
|
toml_file_path.write_text(result_toml)
|
||||||
|
rich.print("[green]TOML file updated.")
|
||||||
|
else:
|
||||||
|
rich.print("[yellow]File left unchanged. Use --force flag to update.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
update_virtualenv()
|
||||||
+29
-10
@@ -1,7 +1,10 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import mkdtemp
|
||||||
from typing import List, Set, Union
|
from typing import List, Set, Union
|
||||||
|
|
||||||
import cibuildwheel
|
import cibuildwheel
|
||||||
@@ -10,9 +13,15 @@ import cibuildwheel.macos
|
|||||||
import cibuildwheel.util
|
import cibuildwheel.util
|
||||||
import cibuildwheel.windows
|
import cibuildwheel.windows
|
||||||
from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
||||||
|
from cibuildwheel.logger import log
|
||||||
from cibuildwheel.options import CommandLineArguments, Options, compute_options
|
from cibuildwheel.options import CommandLineArguments, Options, compute_options
|
||||||
from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never
|
from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never
|
||||||
from cibuildwheel.util import BuildSelector, Unbuffered, detect_ci_provider
|
from cibuildwheel.util import (
|
||||||
|
CIBW_CACHE_PATH,
|
||||||
|
BuildSelector,
|
||||||
|
Unbuffered,
|
||||||
|
detect_ci_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -32,12 +41,11 @@ def main() -> None:
|
|||||||
choices=["auto", "linux", "macos", "windows"],
|
choices=["auto", "linux", "macos", "windows"],
|
||||||
default=os.environ.get("CIBW_PLATFORM", "auto"),
|
default=os.environ.get("CIBW_PLATFORM", "auto"),
|
||||||
help="""
|
help="""
|
||||||
Platform to build for. For "linux" you need docker running, on Mac
|
Platform to build for. Use this option to override the
|
||||||
or Linux. For "macos", you need a Mac machine, and note that this
|
auto-detected platform or to run cibuildwheel on your development
|
||||||
script is going to automatically install MacPython on your system,
|
machine. Specifying "macos" or "windows" only works on that
|
||||||
so don't run on your development machine. For "windows", you need to
|
operating system, but "linux" works on all three, as long as
|
||||||
run in Windows, and it will build and test for all versions of
|
Docker is installed. Default: auto.
|
||||||
Python. Default: auto.
|
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -165,6 +173,9 @@ def main() -> None:
|
|||||||
# Python is buffering by default when running on the CI platforms, giving problems interleaving subprocess call output with unflushed calls to 'print'
|
# Python is buffering by default when running on the CI platforms, giving problems interleaving subprocess call output with unflushed calls to 'print'
|
||||||
sys.stdout = Unbuffered(sys.stdout) # type: ignore[assignment]
|
sys.stdout = Unbuffered(sys.stdout) # type: ignore[assignment]
|
||||||
|
|
||||||
|
# create the cache dir before it gets printed & builds performed
|
||||||
|
CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
print_preamble(platform=platform, options=options, identifiers=identifiers)
|
print_preamble(platform=platform, options=options, identifiers=identifiers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -187,17 +198,23 @@ def main() -> None:
|
|||||||
if not output_dir.exists():
|
if not output_dir.exists():
|
||||||
output_dir.mkdir(parents=True)
|
output_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
|
||||||
|
try:
|
||||||
with cibuildwheel.util.print_new_wheels(
|
with cibuildwheel.util.print_new_wheels(
|
||||||
"\n{n} wheels produced in {m:.0f} minutes:", output_dir
|
"\n{n} wheels produced in {m:.0f} minutes:", output_dir
|
||||||
):
|
):
|
||||||
if platform == "linux":
|
if platform == "linux":
|
||||||
cibuildwheel.linux.build(options)
|
cibuildwheel.linux.build(options, tmp_path)
|
||||||
elif platform == "windows":
|
elif platform == "windows":
|
||||||
cibuildwheel.windows.build(options)
|
cibuildwheel.windows.build(options, tmp_path)
|
||||||
elif platform == "macos":
|
elif platform == "macos":
|
||||||
cibuildwheel.macos.build(options)
|
cibuildwheel.macos.build(options, tmp_path)
|
||||||
else:
|
else:
|
||||||
assert_never(platform)
|
assert_never(platform)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmp_path, ignore_errors=sys.platform.startswith("win"))
|
||||||
|
if tmp_path.exists():
|
||||||
|
log.warning(f"Can't delete temporary folder '{str(tmp_path)}'")
|
||||||
|
|
||||||
|
|
||||||
def print_preamble(platform: str, options: Options, identifiers: List[str]) -> None:
|
def print_preamble(platform: str, options: Options, identifiers: List[str]) -> None:
|
||||||
@@ -218,6 +235,8 @@ def print_preamble(platform: str, options: Options, identifiers: List[str]) -> N
|
|||||||
print(f" platform: {platform!r}")
|
print(f" platform: {platform!r}")
|
||||||
print(textwrap.indent(options.summary(identifiers), " "))
|
print(textwrap.indent(options.summary(identifiers), " "))
|
||||||
|
|
||||||
|
print(f"Cache folder: {CIBW_CACHE_PATH}")
|
||||||
|
|
||||||
warnings = detect_warnings(platform=platform, options=options, identifiers=identifiers)
|
warnings = detect_warnings(platform=platform, options=options, identifiers=identifiers)
|
||||||
if warnings:
|
if warnings:
|
||||||
print("\nWarnings:")
|
print("\nWarnings:")
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ def build_on_docker(
|
|||||||
log.step_end()
|
log.step_end()
|
||||||
|
|
||||||
|
|
||||||
def build(options: Options) -> None:
|
def build(options: Options, tmp_path: Path) -> None:
|
||||||
try:
|
try:
|
||||||
# check docker is installed
|
# check docker is installed
|
||||||
subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL)
|
subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL)
|
||||||
|
|||||||
+62
-88
@@ -4,20 +4,23 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, NamedTuple, Sequence, Set, Tuple, cast
|
from typing import Any, Dict, List, NamedTuple, Sequence, Set, Tuple, cast
|
||||||
|
|
||||||
|
from filelock import FileLock
|
||||||
|
|
||||||
from .architecture import Architecture
|
from .architecture import Architecture
|
||||||
from .environment import ParsedEnvironment
|
from .environment import ParsedEnvironment
|
||||||
from .logger import log
|
from .logger import log
|
||||||
from .options import Options
|
from .options import Options
|
||||||
from .typing import Literal, PathOrStr, assert_never
|
from .typing import Literal, PathOrStr, assert_never
|
||||||
from .util import (
|
from .util import (
|
||||||
|
CIBW_CACHE_PATH,
|
||||||
BuildFrontend,
|
BuildFrontend,
|
||||||
BuildSelector,
|
BuildSelector,
|
||||||
NonPlatformWheelError,
|
NonPlatformWheelError,
|
||||||
call,
|
call,
|
||||||
|
detect_ci_provider,
|
||||||
download,
|
download,
|
||||||
get_build_verbosity_extra_flags,
|
get_build_verbosity_extra_flags,
|
||||||
get_pip_version,
|
get_pip_version,
|
||||||
@@ -26,6 +29,7 @@ from .util import (
|
|||||||
read_python_configs,
|
read_python_configs,
|
||||||
shell,
|
shell,
|
||||||
unwrap,
|
unwrap,
|
||||||
|
virtualenv,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -73,98 +77,75 @@ def get_python_configurations(
|
|||||||
return [c for c in python_configurations if build_selector(c.identifier)]
|
return [c for c in python_configurations if build_selector(c.identifier)]
|
||||||
|
|
||||||
|
|
||||||
SYMLINKS_DIR = Path("/tmp/cibw_bin")
|
def install_cpython(tmp: Path, version: str, url: str) -> Path:
|
||||||
|
installation_path = Path(f"/Library/Frameworks/Python.framework/Versions/{version}")
|
||||||
|
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
|
||||||
def make_symlinks(installation_bin_path: Path, python_executable: str, pip_executable: str) -> None:
|
|
||||||
assert (installation_bin_path / python_executable).exists()
|
|
||||||
|
|
||||||
# Python bin folders on Mac don't symlink `python3` to `python`, and neither
|
|
||||||
# does PyPy for `pypy` or `pypy3`, so we do that so `python` and `pip` always
|
|
||||||
# point to the active configuration.
|
|
||||||
if SYMLINKS_DIR.exists():
|
|
||||||
shutil.rmtree(SYMLINKS_DIR)
|
|
||||||
SYMLINKS_DIR.mkdir(parents=True)
|
|
||||||
|
|
||||||
(SYMLINKS_DIR / "python").symlink_to(installation_bin_path / python_executable)
|
|
||||||
(SYMLINKS_DIR / "python-config").symlink_to(
|
|
||||||
installation_bin_path / (python_executable + "-config")
|
|
||||||
)
|
|
||||||
(SYMLINKS_DIR / "pip").symlink_to(installation_bin_path / pip_executable)
|
|
||||||
|
|
||||||
|
|
||||||
def install_cpython(version: str, url: str) -> Path:
|
|
||||||
installed_system_packages = call("pkgutil", "--pkgs", capture_stdout=True).splitlines()
|
installed_system_packages = call("pkgutil", "--pkgs", capture_stdout=True).splitlines()
|
||||||
|
|
||||||
# if this version of python isn't installed, get it from python.org and install
|
# if this version of python isn't installed, get it from python.org and install
|
||||||
python_package_identifier = f"org.python.Python.PythonFramework-{version}"
|
python_package_identifier = f"org.python.Python.PythonFramework-{version}"
|
||||||
python_executable = "python3"
|
|
||||||
installation_bin_path = Path(f"/Library/Frameworks/Python.framework/Versions/{version}/bin")
|
|
||||||
|
|
||||||
if python_package_identifier not in installed_system_packages:
|
if python_package_identifier not in installed_system_packages:
|
||||||
|
if detect_ci_provider() is None:
|
||||||
|
# if running locally, we don't want to install CPython with sudo
|
||||||
|
# let the user know & provide a link to the installer
|
||||||
|
print(
|
||||||
|
f"Error: CPython {version} is not installed.\n"
|
||||||
|
"cibuildwheel will not perform system-wide installs when running outside of CI.\n"
|
||||||
|
f"To build locally, install CPython {version} on this machine, or, disable this version of Python using CIBW_SKIP=cp{version.replace('.', '')}-macosx_*\n"
|
||||||
|
f"\nDownload link: {url}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
pkg_path = tmp / "Python.pkg"
|
||||||
# download the pkg
|
# download the pkg
|
||||||
download(url, Path("/tmp/Python.pkg"))
|
download(url, pkg_path)
|
||||||
# install
|
# install
|
||||||
call("sudo", "installer", "-pkg", "/tmp/Python.pkg", "-target", "/")
|
call("sudo", "installer", "-pkg", pkg_path, "-target", "/")
|
||||||
|
pkg_path.unlink()
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||||
call(str(installation_bin_path / python_executable), str(install_certifi_script), env=env)
|
call(installation_path / "bin" / "python3", install_certifi_script, env=env)
|
||||||
|
|
||||||
pip_executable = "pip3"
|
return installation_path / "bin" / "python3"
|
||||||
make_symlinks(installation_bin_path, python_executable, pip_executable)
|
|
||||||
|
|
||||||
return installation_bin_path
|
|
||||||
|
|
||||||
|
|
||||||
def install_pypy(version: str, url: str) -> Path:
|
def install_pypy(tmp: Path, version: str, url: str) -> Path:
|
||||||
pypy_tar_bz2 = url.rsplit("/", 1)[-1]
|
pypy_tar_bz2 = url.rsplit("/", 1)[-1]
|
||||||
extension = ".tar.bz2"
|
extension = ".tar.bz2"
|
||||||
assert pypy_tar_bz2.endswith(extension)
|
assert pypy_tar_bz2.endswith(extension)
|
||||||
pypy_base_filename = pypy_tar_bz2[: -len(extension)]
|
installation_path = CIBW_CACHE_PATH / pypy_tar_bz2[: -len(extension)]
|
||||||
installation_path = Path("/tmp") / pypy_base_filename
|
with FileLock(str(installation_path) + ".lock"):
|
||||||
if not installation_path.exists():
|
if not installation_path.exists():
|
||||||
downloaded_tar_bz2 = Path("/tmp") / pypy_tar_bz2
|
downloaded_tar_bz2 = tmp / pypy_tar_bz2
|
||||||
download(url, downloaded_tar_bz2)
|
download(url, downloaded_tar_bz2)
|
||||||
call("tar", "-C", "/tmp", "-xf", downloaded_tar_bz2)
|
installation_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
call("tar", "-C", installation_path.parent, "-xf", downloaded_tar_bz2)
|
||||||
installation_bin_path = installation_path / "bin"
|
downloaded_tar_bz2.unlink()
|
||||||
python_executable = "pypy3"
|
return installation_path / "bin" / "pypy3"
|
||||||
pip_executable = "pip3"
|
|
||||||
make_symlinks(installation_bin_path, python_executable, pip_executable)
|
|
||||||
|
|
||||||
return installation_bin_path
|
|
||||||
|
|
||||||
|
|
||||||
def setup_python(
|
def setup_python(
|
||||||
|
tmp: Path,
|
||||||
python_configuration: PythonConfiguration,
|
python_configuration: PythonConfiguration,
|
||||||
dependency_constraint_flags: Sequence[PathOrStr],
|
dependency_constraint_flags: Sequence[PathOrStr],
|
||||||
environment: ParsedEnvironment,
|
environment: ParsedEnvironment,
|
||||||
build_frontend: BuildFrontend,
|
build_frontend: BuildFrontend,
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, str]:
|
||||||
|
tmp.mkdir()
|
||||||
implementation_id = python_configuration.identifier.split("-")[0]
|
implementation_id = python_configuration.identifier.split("-")[0]
|
||||||
log.step(f"Installing Python {implementation_id}...")
|
log.step(f"Installing Python {implementation_id}...")
|
||||||
|
|
||||||
if implementation_id.startswith("cp"):
|
if implementation_id.startswith("cp"):
|
||||||
installation_bin_path = install_cpython(
|
base_python = install_cpython(tmp, python_configuration.version, python_configuration.url)
|
||||||
python_configuration.version, python_configuration.url
|
|
||||||
)
|
|
||||||
elif implementation_id.startswith("pp"):
|
elif implementation_id.startswith("pp"):
|
||||||
installation_bin_path = install_pypy(python_configuration.version, python_configuration.url)
|
base_python = install_pypy(tmp, python_configuration.version, python_configuration.url)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown Python implementation")
|
raise ValueError("Unknown Python implementation")
|
||||||
|
assert base_python.exists()
|
||||||
|
|
||||||
log.step("Setting up build environment...")
|
log.step("Setting up build environment...")
|
||||||
|
venv_path = tmp / "venv"
|
||||||
env = os.environ.copy()
|
env = virtualenv(base_python, venv_path, dependency_constraint_flags)
|
||||||
env["PATH"] = os.pathsep.join(
|
venv_bin_path = venv_path / "bin"
|
||||||
[
|
assert venv_bin_path.exists()
|
||||||
str(SYMLINKS_DIR),
|
|
||||||
str(installation_bin_path),
|
|
||||||
env["PATH"],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`,
|
# Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`,
|
||||||
# `sys.path`, ... for PyPy: https://foss.heptapod.net/pypy/pypy/issues/3175
|
# `sys.path`, ... for PyPy: https://foss.heptapod.net/pypy/pypy/issues/3175
|
||||||
# Also fix an issue with the shebang of installed scripts inside the
|
# Also fix an issue with the shebang of installed scripts inside the
|
||||||
@@ -176,13 +157,6 @@ def setup_python(
|
|||||||
# we version pip ourselves, so we don't care about pip version checking
|
# we version pip ourselves, so we don't care about pip version checking
|
||||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||||
|
|
||||||
# Install pip
|
|
||||||
|
|
||||||
requires_reinstall = not (installation_bin_path / "pip").exists()
|
|
||||||
if requires_reinstall:
|
|
||||||
# maybe pip isn't installed at all. ensurepip resolves that.
|
|
||||||
call("python", "-m", "ensurepip", env=env, cwd="/tmp")
|
|
||||||
|
|
||||||
# upgrade pip to the version matching our constraints
|
# upgrade pip to the version matching our constraints
|
||||||
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
|
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
|
||||||
call(
|
call(
|
||||||
@@ -190,22 +164,22 @@ def setup_python(
|
|||||||
"-m",
|
"-m",
|
||||||
"pip",
|
"pip",
|
||||||
"install",
|
"install",
|
||||||
"--force-reinstall" if requires_reinstall else "--upgrade",
|
"--upgrade",
|
||||||
"pip",
|
"pip",
|
||||||
*dependency_constraint_flags,
|
*dependency_constraint_flags,
|
||||||
env=env,
|
env=env,
|
||||||
cwd="/tmp",
|
cwd=venv_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply our environment after pip is ready
|
# Apply our environment after pip is ready
|
||||||
env = environment.as_dictionary(prev_environment=env)
|
env = environment.as_dictionary(prev_environment=env)
|
||||||
|
|
||||||
# check what pip version we're on
|
# check what pip version we're on
|
||||||
assert (installation_bin_path / "pip").exists()
|
assert (venv_bin_path / "pip").exists()
|
||||||
call("which", "pip", env=env)
|
call("which", "pip", env=env)
|
||||||
call("pip", "--version", env=env)
|
call("pip", "--version", env=env)
|
||||||
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
|
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
|
||||||
if which_pip != "/tmp/cibw_bin/pip":
|
if which_pip != str(venv_bin_path / "pip"):
|
||||||
print(
|
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.",
|
"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,
|
file=sys.stderr,
|
||||||
@@ -216,7 +190,7 @@ def setup_python(
|
|||||||
call("which", "python", env=env)
|
call("which", "python", env=env)
|
||||||
call("python", "--version", env=env)
|
call("python", "--version", env=env)
|
||||||
which_python = call("which", "python", env=env, capture_stdout=True).strip()
|
which_python = call("which", "python", env=env, capture_stdout=True).strip()
|
||||||
if which_python != "/tmp/cibw_bin/python":
|
if which_python != str(venv_bin_path / "python"):
|
||||||
print(
|
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.",
|
"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,
|
file=sys.stderr,
|
||||||
@@ -295,11 +269,7 @@ def setup_python(
|
|||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def build(options: Options) -> None:
|
def build(options: Options, tmp_path: Path) -> None:
|
||||||
temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel"))
|
|
||||||
built_wheel_dir = temp_dir / "built_wheel"
|
|
||||||
repaired_wheel_dir = temp_dir / "repaired_wheel"
|
|
||||||
|
|
||||||
python_configurations = get_python_configurations(
|
python_configurations = get_python_configurations(
|
||||||
options.globals.build_selector, options.globals.architectures
|
options.globals.build_selector, options.globals.architectures
|
||||||
)
|
)
|
||||||
@@ -321,6 +291,11 @@ def build(options: Options) -> None:
|
|||||||
build_options = options.build_options(config.identifier)
|
build_options = options.build_options(config.identifier)
|
||||||
log.build_start(config.identifier)
|
log.build_start(config.identifier)
|
||||||
|
|
||||||
|
identifier_tmp_dir = tmp_path / config.identifier
|
||||||
|
identifier_tmp_dir.mkdir()
|
||||||
|
built_wheel_dir = identifier_tmp_dir / "built_wheel"
|
||||||
|
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||||
|
|
||||||
config_is_arm64 = config.identifier.endswith("arm64")
|
config_is_arm64 = config.identifier.endswith("arm64")
|
||||||
config_is_universal2 = config.identifier.endswith("universal2")
|
config_is_universal2 = config.identifier.endswith("universal2")
|
||||||
|
|
||||||
@@ -332,6 +307,7 @@ def build(options: Options) -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
env = setup_python(
|
env = setup_python(
|
||||||
|
identifier_tmp_dir / "build",
|
||||||
config,
|
config,
|
||||||
dependency_constraint_flags,
|
dependency_constraint_flags,
|
||||||
build_options.environment,
|
build_options.environment,
|
||||||
@@ -346,9 +322,7 @@ def build(options: Options) -> None:
|
|||||||
shell(before_build_prepared, env=env)
|
shell(before_build_prepared, env=env)
|
||||||
|
|
||||||
log.step("Building wheel...")
|
log.step("Building wheel...")
|
||||||
if built_wheel_dir.exists():
|
built_wheel_dir.mkdir()
|
||||||
shutil.rmtree(built_wheel_dir)
|
|
||||||
built_wheel_dir.mkdir(parents=True)
|
|
||||||
|
|
||||||
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
|
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
|
||||||
|
|
||||||
@@ -390,9 +364,7 @@ def build(options: Options) -> None:
|
|||||||
|
|
||||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||||
|
|
||||||
if repaired_wheel_dir.exists():
|
repaired_wheel_dir.mkdir()
|
||||||
shutil.rmtree(repaired_wheel_dir)
|
|
||||||
repaired_wheel_dir.mkdir(parents=True)
|
|
||||||
|
|
||||||
if built_wheel.name.endswith("none-any.whl"):
|
if built_wheel.name.endswith("none-any.whl"):
|
||||||
raise NonPlatformWheelError()
|
raise NonPlatformWheelError()
|
||||||
@@ -479,7 +451,8 @@ def build(options: Options) -> None:
|
|||||||
# set up a virtual environment to install and test from, to make sure
|
# set up a virtual environment to install and test from, to make sure
|
||||||
# there are no dependencies that were pulled in at build time.
|
# there are no dependencies that were pulled in at build time.
|
||||||
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
|
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
|
||||||
venv_dir = Path(tempfile.mkdtemp())
|
|
||||||
|
venv_dir = identifier_tmp_dir / "venv-test"
|
||||||
|
|
||||||
arch_prefix = []
|
arch_prefix = []
|
||||||
if testing_arch != machine_arch:
|
if testing_arch != machine_arch:
|
||||||
@@ -548,11 +521,12 @@ def build(options: Options) -> None:
|
|||||||
test_command_prepared, cwd=os.environ["HOME"], env=virtualenv_env
|
test_command_prepared, cwd=os.environ["HOME"], env=virtualenv_env
|
||||||
)
|
)
|
||||||
|
|
||||||
# clean up
|
|
||||||
shutil.rmtree(venv_dir)
|
|
||||||
|
|
||||||
# we're all done here; move it to output (overwrite existing)
|
# we're all done here; move it to output (overwrite existing)
|
||||||
shutil.move(str(repaired_wheel), build_options.output_dir)
|
shutil.move(str(repaired_wheel), build_options.output_dir)
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
shutil.rmtree(identifier_tmp_dir)
|
||||||
|
|
||||||
log.build_end()
|
log.build_end()
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
log.step_end_with_error(
|
log.step_end_with_error(
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
version = "20.13.0"
|
||||||
|
url = "https://github.com/pypa/get-virtualenv/blob/20.13.0/public/virtualenv.pyz?raw=true"
|
||||||
+110
-1
@@ -11,6 +11,7 @@ import textwrap
|
|||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import (
|
from typing import (
|
||||||
@@ -21,6 +22,7 @@ from typing import (
|
|||||||
List,
|
List,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
Optional,
|
||||||
|
Sequence,
|
||||||
TextIO,
|
TextIO,
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
@@ -29,10 +31,13 @@ from typing import (
|
|||||||
import bracex
|
import bracex
|
||||||
import certifi
|
import certifi
|
||||||
import tomli
|
import tomli
|
||||||
|
from filelock import FileLock
|
||||||
|
from packaging.requirements import InvalidRequirement, Requirement
|
||||||
from packaging.specifiers import SpecifierSet
|
from packaging.specifiers import SpecifierSet
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
|
from platformdirs import user_cache_path
|
||||||
|
|
||||||
from .typing import Literal, PathOrStr, PlatformName
|
from cibuildwheel.typing import Literal, PathOrStr, PlatformName
|
||||||
|
|
||||||
resources_dir = Path(__file__).parent / "resources"
|
resources_dir = Path(__file__).parent / "resources"
|
||||||
|
|
||||||
@@ -59,6 +64,9 @@ MUSLLINUX_ARCHS = (
|
|||||||
"s390x",
|
"s390x",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DEFAULT_CIBW_CACHE_PATH = user_cache_path(appname="cibuildwheel", appauthor="pypa")
|
||||||
|
CIBW_CACHE_PATH = Path(os.environ.get("CIBW_CACHE_PATH", DEFAULT_CIBW_CACHE_PATH)).resolve()
|
||||||
|
|
||||||
IS_WIN = sys.platform.startswith("win")
|
IS_WIN = sys.platform.startswith("win")
|
||||||
|
|
||||||
|
|
||||||
@@ -451,3 +459,104 @@ def get_pip_version(env: Dict[str, str]) -> str:
|
|||||||
if version.startswith("pip==")
|
if version.startswith("pip==")
|
||||||
)
|
)
|
||||||
return pip_version
|
return pip_version
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def _ensure_virtualenv() -> Path:
|
||||||
|
input_file = resources_dir / "virtualenv.toml"
|
||||||
|
with input_file.open("rb") as f:
|
||||||
|
loaded_file = tomli.load(f)
|
||||||
|
version = str(loaded_file["version"])
|
||||||
|
url = str(loaded_file["url"])
|
||||||
|
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
|
||||||
|
with FileLock(str(path) + ".lock"):
|
||||||
|
if not path.exists():
|
||||||
|
download(url, path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_constraints_for_virtualenv(
|
||||||
|
dependency_constraint_flags: Sequence[PathOrStr],
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Parses the constraints file referenced by `dependency_constraint_flags` and returns a dict where
|
||||||
|
the key is the package name, and the value is the constraint version.
|
||||||
|
If a package version cannot be found, its value is "embed" meaning that virtualenv will install
|
||||||
|
its bundled version, already available locally.
|
||||||
|
The function does not try to be too smart and just handles basic constraints.
|
||||||
|
If it can't get an exact version, the real constraint will be handled by the
|
||||||
|
{macos|windows}.setup_python function.
|
||||||
|
"""
|
||||||
|
assert len(dependency_constraint_flags) in {0, 2}
|
||||||
|
packages = ["pip", "setuptools", "wheel"]
|
||||||
|
constraints_dict = {package: "embed" for package in packages}
|
||||||
|
if len(dependency_constraint_flags) == 2:
|
||||||
|
assert dependency_constraint_flags[0] == "-c"
|
||||||
|
constraint_path = Path(dependency_constraint_flags[1])
|
||||||
|
assert constraint_path.exists()
|
||||||
|
with constraint_path.open() as constraint_file:
|
||||||
|
for line in constraint_file:
|
||||||
|
line = line.strip()
|
||||||
|
if len(line) == 0:
|
||||||
|
continue
|
||||||
|
if line.startswith("#"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
requirement = Requirement(line)
|
||||||
|
package = requirement.name
|
||||||
|
if (
|
||||||
|
package not in packages
|
||||||
|
or requirement.url is not None
|
||||||
|
or requirement.marker is not None
|
||||||
|
or len(requirement.extras) != 0
|
||||||
|
or len(requirement.specifier) != 1
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
specifier = next(iter(requirement.specifier))
|
||||||
|
if specifier.operator != "==":
|
||||||
|
continue
|
||||||
|
constraints_dict[package] = specifier.version
|
||||||
|
except InvalidRequirement:
|
||||||
|
continue
|
||||||
|
return constraints_dict
|
||||||
|
|
||||||
|
|
||||||
|
def virtualenv(
|
||||||
|
python: Path, venv_path: Path, dependency_constraint_flags: Sequence[PathOrStr]
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
assert python.exists()
|
||||||
|
virtualenv_app = _ensure_virtualenv()
|
||||||
|
constraints = _parse_constraints_for_virtualenv(dependency_constraint_flags)
|
||||||
|
additional_flags = [f"--{package}={version}" for package, version in constraints.items()]
|
||||||
|
|
||||||
|
# Using symlinks to pre-installed seed packages is really the fastest way to get a virtual
|
||||||
|
# environment. The initial cost is a bit higher but reusing is much faster.
|
||||||
|
# Windows does not always allow symlinks so just disabling for now.
|
||||||
|
# Requires pip>=19.3 so disabling for "embed" because this means we don't know what's the
|
||||||
|
# version of pip that will end-up installed.
|
||||||
|
# c.f. https://virtualenv.pypa.io/en/latest/cli_interface.html#section-seeder
|
||||||
|
if (
|
||||||
|
not IS_WIN
|
||||||
|
and constraints["pip"] != "embed"
|
||||||
|
and Version(constraints["pip"]) >= Version("19.3")
|
||||||
|
):
|
||||||
|
additional_flags.append("--symlink-app-data")
|
||||||
|
|
||||||
|
call(
|
||||||
|
sys.executable,
|
||||||
|
"-sS", # just the stdlib, https://github.com/pypa/virtualenv/issues/2133#issuecomment-1003710125
|
||||||
|
virtualenv_app,
|
||||||
|
"--activators=",
|
||||||
|
"--no-periodic-update",
|
||||||
|
*additional_flags,
|
||||||
|
"--python",
|
||||||
|
python,
|
||||||
|
venv_path,
|
||||||
|
)
|
||||||
|
if IS_WIN:
|
||||||
|
paths = [str(venv_path), str(venv_path / "Scripts")]
|
||||||
|
else:
|
||||||
|
paths = [str(venv_path / "bin")]
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PATH"] = os.pathsep.join(paths + [env["PATH"]])
|
||||||
|
return env
|
||||||
|
|||||||
+61
-74
@@ -2,11 +2,12 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, NamedTuple, Optional, Sequence, Set
|
from typing import Dict, List, NamedTuple, Optional, Sequence, Set
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from filelock import FileLock
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
|
|
||||||
from .architecture import Architecture
|
from .architecture import Architecture
|
||||||
@@ -15,6 +16,7 @@ from .logger import log
|
|||||||
from .options import Options
|
from .options import Options
|
||||||
from .typing import PathOrStr, assert_never
|
from .typing import PathOrStr, assert_never
|
||||||
from .util import (
|
from .util import (
|
||||||
|
CIBW_CACHE_PATH,
|
||||||
BuildFrontend,
|
BuildFrontend,
|
||||||
BuildSelector,
|
BuildSelector,
|
||||||
NonPlatformWheelError,
|
NonPlatformWheelError,
|
||||||
@@ -25,12 +27,11 @@ from .util import (
|
|||||||
prepare_command,
|
prepare_command,
|
||||||
read_python_configs,
|
read_python_configs,
|
||||||
shell,
|
shell,
|
||||||
|
virtualenv,
|
||||||
)
|
)
|
||||||
|
|
||||||
CIBW_INSTALL_PATH = Path("C:\\cibw")
|
|
||||||
|
|
||||||
|
def get_nuget_args(version: str, arch: str, output_directory: Path) -> List[str]:
|
||||||
def get_nuget_args(version: str, arch: str) -> List[str]:
|
|
||||||
platform_suffix = {"32": "x86", "64": "", "ARM64": "arm64"}
|
platform_suffix = {"32": "x86", "64": "", "ARM64": "arm64"}
|
||||||
python_name = "python" + platform_suffix[arch]
|
python_name = "python" + platform_suffix[arch]
|
||||||
return [
|
return [
|
||||||
@@ -40,7 +41,7 @@ def get_nuget_args(version: str, arch: str) -> List[str]:
|
|||||||
"-FallbackSource",
|
"-FallbackSource",
|
||||||
"https://api.nuget.org/v3/index.json",
|
"https://api.nuget.org/v3/index.json",
|
||||||
"-OutputDirectory",
|
"-OutputDirectory",
|
||||||
str(CIBW_INSTALL_PATH / "python"),
|
str(output_directory),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -77,82 +78,70 @@ def extract_zip(zip_src: Path, dest: Path) -> None:
|
|||||||
zip_.extractall(dest)
|
zip_.extractall(dest)
|
||||||
|
|
||||||
|
|
||||||
def install_cpython(version: str, arch: str, nuget: Path) -> Path:
|
@lru_cache(maxsize=None)
|
||||||
nuget_args = get_nuget_args(version, arch)
|
def _ensure_nuget() -> Path:
|
||||||
installation_path = Path(nuget_args[-1]) / (nuget_args[0] + "." + version) / "tools"
|
nuget = CIBW_CACHE_PATH / "nuget.exe"
|
||||||
|
with FileLock(str(nuget) + ".lock"):
|
||||||
|
if not nuget.exists():
|
||||||
|
download("https://dist.nuget.org/win-x86-commandline/latest/nuget.exe", nuget)
|
||||||
|
return nuget
|
||||||
|
|
||||||
|
|
||||||
|
def install_cpython(version: str, arch: str) -> Path:
|
||||||
|
base_output_dir = CIBW_CACHE_PATH / "nuget-cpython"
|
||||||
|
nuget_args = get_nuget_args(version, arch, base_output_dir)
|
||||||
|
installation_path = base_output_dir / (nuget_args[0] + "." + version) / "tools"
|
||||||
|
with FileLock(str(base_output_dir) + f"-{version}-{arch}.lock"):
|
||||||
|
if not installation_path.exists():
|
||||||
|
nuget = _ensure_nuget()
|
||||||
call(nuget, "install", *nuget_args)
|
call(nuget, "install", *nuget_args)
|
||||||
# "python3" is not included in the vanilla nuget package,
|
return installation_path / "python.exe"
|
||||||
# though it can be present if modified (like on Azure).
|
|
||||||
if not (installation_path / "python3.exe").exists():
|
|
||||||
(installation_path / "python3.exe").symlink_to(installation_path / "python.exe")
|
|
||||||
return installation_path
|
|
||||||
|
|
||||||
|
|
||||||
def install_pypy(version: str, arch: str, url: str) -> Path:
|
def install_pypy(tmp: Path, arch: str, url: str) -> Path:
|
||||||
assert arch == "64" and "win64" in url
|
assert arch == "64" and "win64" in url
|
||||||
# Inside the PyPy zip file is a directory with the same name
|
# Inside the PyPy zip file is a directory with the same name
|
||||||
zip_filename = url.rsplit("/", 1)[-1]
|
zip_filename = url.rsplit("/", 1)[-1]
|
||||||
extension = ".zip"
|
extension = ".zip"
|
||||||
assert zip_filename.endswith(extension)
|
assert zip_filename.endswith(extension)
|
||||||
installation_path = CIBW_INSTALL_PATH / zip_filename[: -len(extension)]
|
installation_path = CIBW_CACHE_PATH / zip_filename[: -len(extension)]
|
||||||
|
with FileLock(str(installation_path) + ".lock"):
|
||||||
if not installation_path.exists():
|
if not installation_path.exists():
|
||||||
pypy_zip = CIBW_INSTALL_PATH / zip_filename
|
pypy_zip = tmp / zip_filename
|
||||||
download(url, pypy_zip)
|
download(url, pypy_zip)
|
||||||
# Extract to the parent directory because the zip file still contains a directory
|
# Extract to the parent directory because the zip file still contains a directory
|
||||||
extract_zip(pypy_zip, installation_path.parent)
|
extract_zip(pypy_zip, installation_path.parent)
|
||||||
return installation_path
|
return installation_path / "python.exe"
|
||||||
|
|
||||||
|
|
||||||
def setup_python(
|
def setup_python(
|
||||||
|
tmp: Path,
|
||||||
python_configuration: PythonConfiguration,
|
python_configuration: PythonConfiguration,
|
||||||
dependency_constraint_flags: Sequence[PathOrStr],
|
dependency_constraint_flags: Sequence[PathOrStr],
|
||||||
environment: ParsedEnvironment,
|
environment: ParsedEnvironment,
|
||||||
build_frontend: BuildFrontend,
|
build_frontend: BuildFrontend,
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, str]:
|
||||||
|
tmp.mkdir()
|
||||||
nuget = CIBW_INSTALL_PATH / "nuget.exe"
|
|
||||||
if not nuget.exists():
|
|
||||||
log.step("Downloading nuget...")
|
|
||||||
download("https://dist.nuget.org/win-x86-commandline/latest/nuget.exe", nuget)
|
|
||||||
|
|
||||||
implementation_id = python_configuration.identifier.split("-")[0]
|
implementation_id = python_configuration.identifier.split("-")[0]
|
||||||
log.step(f"Installing Python {implementation_id}...")
|
log.step(f"Installing Python {implementation_id}...")
|
||||||
|
|
||||||
if implementation_id.startswith("cp"):
|
if implementation_id.startswith("cp"):
|
||||||
installation_path = install_cpython(
|
base_python = install_cpython(python_configuration.version, python_configuration.arch)
|
||||||
python_configuration.version, python_configuration.arch, nuget
|
|
||||||
)
|
|
||||||
elif implementation_id.startswith("pp"):
|
elif implementation_id.startswith("pp"):
|
||||||
assert python_configuration.url is not None
|
assert python_configuration.url is not None
|
||||||
installation_path = install_pypy(
|
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
|
||||||
python_configuration.version, python_configuration.arch, python_configuration.url
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown Python implementation")
|
raise ValueError("Unknown Python implementation")
|
||||||
|
assert base_python.exists()
|
||||||
assert (installation_path / "python.exe").exists()
|
|
||||||
|
|
||||||
log.step("Setting up build environment...")
|
log.step("Setting up build environment...")
|
||||||
|
venv_path = tmp / "venv"
|
||||||
|
env = virtualenv(base_python, venv_path, dependency_constraint_flags)
|
||||||
|
|
||||||
# set up PATH and environment variables for run_with_env
|
# set up environment variables for run_with_env
|
||||||
env = os.environ.copy()
|
|
||||||
env["PYTHON_VERSION"] = python_configuration.version
|
env["PYTHON_VERSION"] = python_configuration.version
|
||||||
env["PYTHON_ARCH"] = python_configuration.arch
|
env["PYTHON_ARCH"] = python_configuration.arch
|
||||||
env["PATH"] = os.pathsep.join(
|
|
||||||
[str(installation_path), str(installation_path / "Scripts"), env["PATH"]]
|
|
||||||
)
|
|
||||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||||
|
|
||||||
log.step("Installing build tools...")
|
|
||||||
|
|
||||||
# Install pip
|
|
||||||
|
|
||||||
requires_reinstall = not (installation_path / "Scripts" / "pip.exe").exists()
|
|
||||||
|
|
||||||
if requires_reinstall:
|
|
||||||
# maybe pip isn't installed at all. ensurepip resolves that.
|
|
||||||
call("python", "-m", "ensurepip", env=env, cwd=CIBW_INSTALL_PATH)
|
|
||||||
|
|
||||||
# pip older than 21.3 builds executables such as pip.exe for x64 platform.
|
# pip older than 21.3 builds executables such as pip.exe for x64 platform.
|
||||||
# The first re-install of pip updates pip module but builds pip.exe using
|
# The first re-install of pip updates pip module but builds pip.exe using
|
||||||
# the old pip which still generates x64 executable. But the second
|
# the old pip which still generates x64 executable. But the second
|
||||||
@@ -170,7 +159,7 @@ def setup_python(
|
|||||||
"pip",
|
"pip",
|
||||||
*dependency_constraint_flags,
|
*dependency_constraint_flags,
|
||||||
env=env,
|
env=env,
|
||||||
cwd=CIBW_INSTALL_PATH,
|
cwd=venv_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
# upgrade pip to the version matching our constraints
|
# upgrade pip to the version matching our constraints
|
||||||
@@ -180,11 +169,11 @@ def setup_python(
|
|||||||
"-m",
|
"-m",
|
||||||
"pip",
|
"pip",
|
||||||
"install",
|
"install",
|
||||||
"--force-reinstall" if requires_reinstall else "--upgrade",
|
"--upgrade",
|
||||||
"pip",
|
"pip",
|
||||||
*dependency_constraint_flags,
|
*dependency_constraint_flags,
|
||||||
env=env,
|
env=env,
|
||||||
cwd=CIBW_INSTALL_PATH,
|
cwd=venv_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
# update env with results from CIBW_ENVIRONMENT
|
# update env with results from CIBW_ENVIRONMENT
|
||||||
@@ -195,7 +184,7 @@ def setup_python(
|
|||||||
call("python", "--version", env=env)
|
call("python", "--version", env=env)
|
||||||
call("python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\"", env=env)
|
call("python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\"", env=env)
|
||||||
where_python = call("where", "python", env=env, capture_stdout=True).splitlines()[0].strip()
|
where_python = call("where", "python", env=env, capture_stdout=True).splitlines()[0].strip()
|
||||||
if where_python != str(installation_path / "python.exe"):
|
if where_python != str(venv_path / "Scripts" / "python.exe"):
|
||||||
print(
|
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.",
|
"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,
|
file=sys.stderr,
|
||||||
@@ -203,9 +192,9 @@ def setup_python(
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# check what pip version we're on
|
# check what pip version we're on
|
||||||
assert (installation_path / "Scripts" / "pip.exe").exists()
|
assert (venv_path / "Scripts" / "pip.exe").exists()
|
||||||
where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip()
|
where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip()
|
||||||
if where_pip.strip() != str(installation_path / "Scripts" / "pip.exe"):
|
if where_pip.strip() != str(venv_path / "Scripts" / "pip.exe"):
|
||||||
print(
|
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.",
|
"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,
|
file=sys.stderr,
|
||||||
@@ -214,6 +203,7 @@ def setup_python(
|
|||||||
|
|
||||||
call("pip", "--version", env=env)
|
call("pip", "--version", env=env)
|
||||||
|
|
||||||
|
log.step("Installing build tools...")
|
||||||
if build_frontend == "pip":
|
if build_frontend == "pip":
|
||||||
call(
|
call(
|
||||||
"pip",
|
"pip",
|
||||||
@@ -239,11 +229,7 @@ def setup_python(
|
|||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def build(options: Options) -> None:
|
def build(options: Options, tmp_path: Path) -> None:
|
||||||
temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel"))
|
|
||||||
built_wheel_dir = temp_dir / "built_wheel"
|
|
||||||
repaired_wheel_dir = temp_dir / "repaired_wheel"
|
|
||||||
|
|
||||||
python_configurations = get_python_configurations(
|
python_configurations = get_python_configurations(
|
||||||
options.globals.build_selector, options.globals.architectures
|
options.globals.build_selector, options.globals.architectures
|
||||||
)
|
)
|
||||||
@@ -264,6 +250,11 @@ def build(options: Options) -> None:
|
|||||||
build_options = options.build_options(config.identifier)
|
build_options = options.build_options(config.identifier)
|
||||||
log.build_start(config.identifier)
|
log.build_start(config.identifier)
|
||||||
|
|
||||||
|
identifier_tmp_dir = tmp_path / config.identifier
|
||||||
|
identifier_tmp_dir.mkdir()
|
||||||
|
built_wheel_dir = identifier_tmp_dir / "built_wheel"
|
||||||
|
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||||
|
|
||||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
dependency_constraint_flags: Sequence[PathOrStr] = []
|
||||||
if build_options.dependency_constraints:
|
if build_options.dependency_constraints:
|
||||||
dependency_constraint_flags = [
|
dependency_constraint_flags = [
|
||||||
@@ -273,6 +264,7 @@ def build(options: Options) -> None:
|
|||||||
|
|
||||||
# install Python
|
# install Python
|
||||||
env = setup_python(
|
env = setup_python(
|
||||||
|
identifier_tmp_dir / "build",
|
||||||
config,
|
config,
|
||||||
dependency_constraint_flags,
|
dependency_constraint_flags,
|
||||||
build_options.environment,
|
build_options.environment,
|
||||||
@@ -288,9 +280,7 @@ def build(options: Options) -> None:
|
|||||||
shell(before_build_prepared, env=env)
|
shell(before_build_prepared, env=env)
|
||||||
|
|
||||||
log.step("Building wheel...")
|
log.step("Building wheel...")
|
||||||
if built_wheel_dir.exists():
|
built_wheel_dir.mkdir()
|
||||||
shutil.rmtree(built_wheel_dir)
|
|
||||||
built_wheel_dir.mkdir(parents=True)
|
|
||||||
|
|
||||||
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
|
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
|
||||||
|
|
||||||
@@ -320,12 +310,10 @@ def build(options: Options) -> None:
|
|||||||
# in uhi. After probably pip 21.2, we can use uri. For
|
# in uhi. After probably pip 21.2, we can use uri. For
|
||||||
# now, use a temporary file.
|
# now, use a temporary file.
|
||||||
if " " in str(constraints_path):
|
if " " in str(constraints_path):
|
||||||
tmp_file = tempfile.NamedTemporaryFile(
|
assert " " not in str(identifier_tmp_dir)
|
||||||
"w", suffix="constraints.txt", delete=False, dir=CIBW_INSTALL_PATH
|
tmp_file = identifier_tmp_dir / "constraints.txt"
|
||||||
)
|
tmp_file.write_bytes(constraints_path.read_bytes())
|
||||||
with tmp_file as new_constraints_file, open(constraints_path) as f:
|
constraints_path = tmp_file
|
||||||
new_constraints_file.write(f.read())
|
|
||||||
constraints_path = Path(new_constraints_file.name)
|
|
||||||
|
|
||||||
build_env["PIP_CONSTRAINT"] = str(constraints_path)
|
build_env["PIP_CONSTRAINT"] = str(constraints_path)
|
||||||
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
|
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
|
||||||
@@ -345,9 +333,7 @@ def build(options: Options) -> None:
|
|||||||
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
built_wheel = next(built_wheel_dir.glob("*.whl"))
|
||||||
|
|
||||||
# repair the wheel
|
# repair the wheel
|
||||||
if repaired_wheel_dir.exists():
|
repaired_wheel_dir.mkdir()
|
||||||
shutil.rmtree(repaired_wheel_dir)
|
|
||||||
repaired_wheel_dir.mkdir(parents=True)
|
|
||||||
|
|
||||||
if built_wheel.name.endswith("none-any.whl"):
|
if built_wheel.name.endswith("none-any.whl"):
|
||||||
raise NonPlatformWheelError()
|
raise NonPlatformWheelError()
|
||||||
@@ -368,7 +354,7 @@ def build(options: Options) -> None:
|
|||||||
# set up a virtual environment to install and test from, to make sure
|
# set up a virtual environment to install and test from, to make sure
|
||||||
# there are no dependencies that were pulled in at build time.
|
# there are no dependencies that were pulled in at build time.
|
||||||
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
|
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
|
||||||
venv_dir = Path(tempfile.mkdtemp())
|
venv_dir = identifier_tmp_dir / "venv-test"
|
||||||
|
|
||||||
# Use --no-download to ensure determinism by using seed libraries
|
# Use --no-download to ensure determinism by using seed libraries
|
||||||
# built into virtualenv
|
# built into virtualenv
|
||||||
@@ -415,13 +401,14 @@ def build(options: Options) -> None:
|
|||||||
)
|
)
|
||||||
shell(test_command_prepared, cwd="c:\\", env=virtualenv_env)
|
shell(test_command_prepared, cwd="c:\\", env=virtualenv_env)
|
||||||
|
|
||||||
|
# we're all done here; move it to output (remove if already exists)
|
||||||
|
shutil.move(str(repaired_wheel), build_options.output_dir)
|
||||||
|
|
||||||
# clean up
|
# clean up
|
||||||
# (we ignore errors because occasionally Windows fails to unlink a file and we
|
# (we ignore errors because occasionally Windows fails to unlink a file and we
|
||||||
# don't want to abort a build because of that)
|
# don't want to abort a build because of that)
|
||||||
shutil.rmtree(venv_dir, ignore_errors=True)
|
shutil.rmtree(identifier_tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
# we're all done here; move it to output (remove if already exists)
|
|
||||||
shutil.move(str(repaired_wheel), build_options.output_dir)
|
|
||||||
log.build_end()
|
log.build_end()
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
log.step_end_with_error(
|
log.step_end_with_error(
|
||||||
|
|||||||
+4
-4
@@ -174,14 +174,13 @@ Default: `auto`
|
|||||||
|
|
||||||
`auto` will auto-detect platform using environment variables, such as `TRAVIS_OS_NAME`/`APPVEYOR`/`CIRCLECI`.
|
`auto` will auto-detect platform using environment variables, such as `TRAVIS_OS_NAME`/`APPVEYOR`/`CIRCLECI`.
|
||||||
|
|
||||||
- For `linux`, you need Docker running, on macOS or Linux.
|
- For `linux`, you need Docker running, on Linux, macOS, or Windows.
|
||||||
- For `macos`, you need a Mac machine. Note that cibuildwheel is going to install MacPython on your system, so you probably don't want to run this on your development machine.
|
- For `macos` and `windows`, you need to be running on the respective system, with a working compiler toolchain installed - Xcode Command Line tools for macOS, and MSVC for Windows.
|
||||||
- For `windows`, you need to run in Windows. cibuildwheel will install required versions of Python to `C:\cibw\python` using NuGet.
|
|
||||||
|
|
||||||
This option can also be set using the [command-line option](#command-line) `--platform`. This option is not available in the `pyproject.toml` config.
|
This option can also be set using the [command-line option](#command-line) `--platform`. This option is not available in the `pyproject.toml` config.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
If you have Docker installed, you can locally debug your cibuildwheel Linux config, instead of pushing to CI to test every change. For example:
|
You can use this option to locally debug your cibuildwheel config, instead of pushing to CI to test every change. For example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export CIBW_BUILD='cp37-*'
|
export CIBW_BUILD='cp37-*'
|
||||||
@@ -189,6 +188,7 @@ This option can also be set using the [command-line option](#command-line) `--pl
|
|||||||
cibuildwheel --platform linux .
|
cibuildwheel --platform linux .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This is even more convenient if you store your cibuildwheel config in [`pyproject.toml`](#configuration-file).
|
||||||
|
|
||||||
### `CIBW_BUILD`, `CIBW_SKIP` {: #build-skip}
|
### `CIBW_BUILD`, `CIBW_SKIP` {: #build-skip}
|
||||||
|
|
||||||
|
|||||||
+86
-21
@@ -5,31 +5,49 @@ title: 'Setup'
|
|||||||
# Run cibuildwheel locally (optional) {: #local}
|
# Run cibuildwheel locally (optional) {: #local}
|
||||||
|
|
||||||
Before getting to CI setup, it can be convenient to test cibuildwheel
|
Before getting to CI setup, it can be convenient to test cibuildwheel
|
||||||
locally to quickly iterate and track down issues. If you've got
|
locally to quickly iterate and track down issues without even touching CI.
|
||||||
[Docker](https://www.docker.com/products/docker-desktop) installed on
|
|
||||||
your development machine, you can run a Linux build without even
|
|
||||||
touching CI.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
You can run the Linux build on any platform. Even Windows can run
|
|
||||||
Linux containers these days, but there are a few hoops to jump
|
|
||||||
through. Check [this document](https://docs.microsoft.com/en-us/virtualization/windowscontainers/quick-start/quick-start-windows-10-linux)
|
|
||||||
for more info.
|
|
||||||
|
|
||||||
This is convenient as it's quicker to iterate, and because the builds are
|
|
||||||
happening in manylinux Docker containers, they're perfectly reproducible.
|
|
||||||
|
|
||||||
Install cibuildwheel and run a build like this:
|
Install cibuildwheel and run a build like this:
|
||||||
|
|
||||||
```sh
|
!!! tab "Linux"
|
||||||
pip install cibuildwheel
|
|
||||||
cibuildwheel --platform linux
|
|
||||||
```
|
|
||||||
|
|
||||||
Or, using [pipx](https://github.com/pypa/pipx):
|
Using [pipx](https://github.com/pypa/pipx):
|
||||||
```sh
|
```sh
|
||||||
pipx run cibuildwheel --platform linux
|
pipx run cibuildwheel --platform linux
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or,
|
||||||
|
```sh
|
||||||
|
pip install cibuildwheel
|
||||||
|
cibuildwheel --platform linux
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tab "macOS"
|
||||||
|
|
||||||
|
Using [pipx](https://github.com/pypa/pipx):
|
||||||
|
```sh
|
||||||
|
pipx run cibuildwheel --platform macos
|
||||||
|
```
|
||||||
|
|
||||||
|
Or,
|
||||||
|
```sh
|
||||||
|
pip install cibuildwheel
|
||||||
|
cibuildwheel --platform macos
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
!!! tab "Windows"
|
||||||
|
|
||||||
|
Using [pipx](https://github.com/pypa/pipx):
|
||||||
|
```bat
|
||||||
|
pipx run cibuildwheel --platform windows
|
||||||
|
```
|
||||||
|
|
||||||
|
Or,
|
||||||
|
```bat
|
||||||
|
pip install cibuildwheel
|
||||||
|
cibuildwheel --platform windows
|
||||||
|
```
|
||||||
|
|
||||||
You should see the builds taking place. You can experiment with options using environment variables or pyproject.toml.
|
You should see the builds taking place. You can experiment with options using environment variables or pyproject.toml.
|
||||||
|
|
||||||
@@ -71,6 +89,53 @@ You should see the builds taking place. You can experiment with options using en
|
|||||||
cibuildwheel --platform linux
|
cibuildwheel --platform linux
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Linux builds
|
||||||
|
|
||||||
|
If you've got [Docker](https://www.docker.com/products/docker-desktop) installed on
|
||||||
|
your development machine, you can run a Linux build.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
You can run the Linux build on any platform. Even Windows can run
|
||||||
|
Linux containers these days, but there are a few hoops to jump
|
||||||
|
through. Check [this document](https://docs.microsoft.com/en-us/virtualization/windowscontainers/quick-start/quick-start-windows-10-linux)
|
||||||
|
for more info.
|
||||||
|
|
||||||
|
Because the builds are happening in manylinux Docker containers,
|
||||||
|
they're perfectly reproducible.
|
||||||
|
|
||||||
|
The only side effect to your system will be docker images being pulled.
|
||||||
|
|
||||||
|
## macOS / Windows builds
|
||||||
|
|
||||||
|
Pre-requisite: you need to have native build tools installed.
|
||||||
|
|
||||||
|
Because the builds are happening without full isolation, there might be some
|
||||||
|
differences compared to CI builds (Xcode version, Visual Studio version,
|
||||||
|
OS version, local files, ...) that might prevent you from finding an issue only
|
||||||
|
seen in CI.
|
||||||
|
|
||||||
|
In order to speed-up builds, cibuildwheel will cache the tools it needs to be
|
||||||
|
reused for future builds. The folder used for caching is system/user dependent and is
|
||||||
|
reported in the printed preamble of each run (e.g. "Cache folder: /Users/Matt/Library/Caches/cibuildwheel").
|
||||||
|
|
||||||
|
You can override the cache folder using the ``CIBW_CACHE_PATH`` environment variable.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
cibuildwheel uses official python.org macOS installers for CPython but
|
||||||
|
those can only be installed globally.
|
||||||
|
|
||||||
|
In order not to mess with your system, cibuildwheel won't install those if they are
|
||||||
|
missing. Instead, it will error out with a message to let you install the missing
|
||||||
|
CPython:
|
||||||
|
|
||||||
|
```console
|
||||||
|
Error: CPython 3.6 is not installed.
|
||||||
|
cibuildwheel will not perform system-wide installs when running outside of CI.
|
||||||
|
To build locally, install CPython 3.6 on this machine, or, disable this version of Python using CIBW_SKIP=cp36-macosx_*
|
||||||
|
|
||||||
|
Download link: https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# Configure a CI service
|
# Configure a CI service
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -56,11 +56,12 @@ def update_constraints(session: nox.Session) -> None:
|
|||||||
@nox.session
|
@nox.session
|
||||||
def update_pins(session: nox.Session) -> None:
|
def update_pins(session: nox.Session) -> None:
|
||||||
"""
|
"""
|
||||||
Update the python and docker pins version inplace.
|
Update the python, docker and virtualenv pins version inplace.
|
||||||
"""
|
"""
|
||||||
session.install("-e", ".[bin]")
|
session.install("-e", ".[bin]")
|
||||||
session.run("python", "bin/update_pythons.py", "--force")
|
session.run("python", "bin/update_pythons.py", "--force")
|
||||||
session.run("python", "bin/update_docker.py")
|
session.run("python", "bin/update_docker.py")
|
||||||
|
session.run("python", "bin/update_virtualenv.py", "--force")
|
||||||
|
|
||||||
|
|
||||||
@nox.session
|
@nox.session
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ install_requires =
|
|||||||
bashlex!=0.13
|
bashlex!=0.13
|
||||||
bracex
|
bracex
|
||||||
certifi
|
certifi
|
||||||
|
filelock
|
||||||
packaging
|
packaging
|
||||||
|
platformdirs
|
||||||
tomli
|
tomli
|
||||||
dataclasses;python_version < '3.7'
|
dataclasses;python_version < '3.7'
|
||||||
typing-extensions>=3.10.0.0;python_version < '3.8'
|
typing-extensions>=3.10.0.0;python_version < '3.8'
|
||||||
|
|||||||
Reference in New Issue
Block a user