Merge pull request #1096 from henryiii/henryiii/feat/sdist

feat: SDist builds
This commit is contained in:
Joe Rickerby
2022-04-29 11:52:08 +01:00
committed by GitHub
11 changed files with 264 additions and 34 deletions
+50 -9
View File
@@ -2,6 +2,8 @@ import argparse
import os import os
import shutil import shutil
import sys import sys
import tarfile
import tempfile
import textwrap import textwrap
from pathlib import Path from pathlib import Path
from tempfile import mkdtemp from tempfile import mkdtemp
@@ -20,13 +22,12 @@ from cibuildwheel.util import (
CIBW_CACHE_PATH, CIBW_CACHE_PATH,
BuildSelector, BuildSelector,
Unbuffered, Unbuffered,
chdir,
detect_ci_provider, detect_ci_provider,
) )
def main() -> None: def main() -> None:
platform: PlatformName
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Build wheels for all the platforms.", description="Build wheels for all the platforms.",
epilog=""" epilog="""
@@ -65,6 +66,8 @@ def main() -> None:
parser.add_argument( parser.add_argument(
"--output-dir", "--output-dir",
type=Path,
default=Path(os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse")),
help="Destination folder for the wheels. Default: wheelhouse.", help="Destination folder for the wheels. Default: wheelhouse.",
) )
@@ -72,20 +75,26 @@ def main() -> None:
"--config-file", "--config-file",
default="", default="",
help=""" help="""
TOML config file. Default: "", meaning {package}/pyproject.toml, TOML config file. Default: "", meaning {package}/pyproject.toml, if
if it exists. it exists. To refer to a project inside your project, use {package};
this matters if you build from an SDist.
""", """,
) )
parser.add_argument( parser.add_argument(
"package_dir", "package_dir",
default=".", metavar="PACKAGE",
default=Path("."),
type=Path,
nargs="?", nargs="?",
help=""" help="""
Path to the package that you want wheels for. Must be a subdirectory of Path to the package that you want wheels for. Default: the working
the working directory. When set, the working directory is still directory. Can be a directory inside the working directory, or an
considered the 'project' and is copied into the Docker container on sdist. When set to a directory, the working directory is still
Linux. Default: the working directory. considered the 'project' and is copied into the Docker container
on Linux. When set to a tar.gz sdist file, --config-file
and --output-dir are relative to the current directory, and other
paths are relative to the expanded SDist directory.
""", """,
) )
@@ -109,6 +118,38 @@ def main() -> None:
args = parser.parse_args(namespace=CommandLineArguments()) args = parser.parse_args(namespace=CommandLineArguments())
args.package_dir = args.package_dir.resolve()
# This are always relative to the base directory, even in SDist builds
args.output_dir = args.output_dir.resolve()
# Standard builds if a directory or non-existent path is given
if not args.package_dir.is_file() and not args.package_dir.name.endswith("tar.gz"):
build_in_directory(args)
return
# Tarfile builds require extraction and changing the directory
with tempfile.TemporaryDirectory(prefix="cibw-sdist-") as temp_dir_str:
temp_dir = Path(temp_dir_str)
with tarfile.open(args.package_dir) as tar:
tar.extractall(path=temp_dir)
# The extract directory is now the project dir
try:
(project_dir,) = temp_dir.iterdir()
except ValueError:
raise SystemExit("invalid sdist: didn't contain a single dir") from None
# This is now the new package dir
args.package_dir = project_dir.resolve()
with chdir(temp_dir):
build_in_directory(args)
def build_in_directory(args: CommandLineArguments) -> None:
platform: PlatformName
if args.platform != "auto": if args.platform != "auto":
platform = args.platform platform = args.platform
else: else:
+9 -11
View File
@@ -8,7 +8,7 @@ from pathlib import Path
from typing import ( from typing import (
Any, Any,
Dict, Dict,
Iterator, Generator,
List, List,
Mapping, Mapping,
NamedTuple, NamedTuple,
@@ -22,6 +22,7 @@ if sys.version_info >= (3, 11):
import tomllib import tomllib
else: else:
import tomli as tomllib import tomli as tomllib
from packaging.specifiers import SpecifierSet from packaging.specifiers import SpecifierSet
from .architecture import Architecture from .architecture import Architecture
@@ -36,6 +37,7 @@ from .util import (
DependencyConstraints, DependencyConstraints,
TestSelector, TestSelector,
cached_property, cached_property,
format_safe,
resources_dir, resources_dir,
selector_matches, selector_matches,
strtobool, strtobool,
@@ -46,9 +48,9 @@ from .util import (
class CommandLineArguments: class CommandLineArguments:
platform: Literal["auto", "linux", "macos", "windows"] platform: Literal["auto", "linux", "macos", "windows"]
archs: Optional[str] archs: Optional[str]
output_dir: Optional[str] output_dir: Path
config_file: str config_file: str
package_dir: str package_dir: Path
print_build_identifiers: bool print_build_identifiers: bool
allow_empty: bool allow_empty: bool
prerelease_pythons: bool prerelease_pythons: bool
@@ -263,7 +265,7 @@ class OptionsReader:
] ]
@contextmanager @contextmanager
def identifier(self, identifier: Optional[str]) -> Iterator[None]: def identifier(self, identifier: Optional[str]) -> Generator[None, None, None]:
self.current_identifier = identifier self.current_identifier = identifier
try: try:
yield yield
@@ -344,7 +346,7 @@ class Options:
args = self.command_line_arguments args = self.command_line_arguments
if args.config_file: if args.config_file:
return Path(args.config_file.format(package=args.package_dir)) return Path(format_safe(args.config_file, package=args.package_dir))
# return pyproject.toml, if it's available # return pyproject.toml, if it's available
pyproject_toml_path = Path(args.package_dir) / "pyproject.toml" pyproject_toml_path = Path(args.package_dir) / "pyproject.toml"
@@ -361,12 +363,8 @@ class Options:
@property @property
def globals(self) -> GlobalOptions: def globals(self) -> GlobalOptions:
args = self.command_line_arguments args = self.command_line_arguments
package_dir = Path(args.package_dir) package_dir = args.package_dir
output_dir = Path( output_dir = args.output_dir
args.output_dir
if args.output_dir is not None
else os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse")
)
build_config = self.reader.get("build", env_plat=False, sep=" ") or "*" build_config = self.reader.get("build", env_plat=False, sep=" ") or "*"
skip_config = self.reader.get("skip", env_plat=False, sep=" ") skip_config = self.reader.get("skip", env_plat=False, sep=" ")
+17 -2
View File
@@ -19,13 +19,14 @@ from typing import (
Any, Any,
ClassVar, ClassVar,
Dict, Dict,
Generator,
Iterable, Iterable,
Iterator,
List, List,
NamedTuple, NamedTuple,
Optional, Optional,
Sequence, Sequence,
TextIO, TextIO,
Union,
cast, cast,
overload, overload,
) )
@@ -58,6 +59,7 @@ __all__ = [
"selector_matches", "selector_matches",
"strtobool", "strtobool",
"cached_property", "cached_property",
"chdir",
] ]
resources_dir: Final = Path(__file__).parent / "resources" resources_dir: Final = Path(__file__).parent / "resources"
@@ -414,7 +416,7 @@ def unwrap(text: str) -> str:
@contextlib.contextmanager @contextlib.contextmanager
def print_new_wheels(msg: str, output_dir: Path) -> Iterator[None]: def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
""" """
Prints the new items in a directory upon exiting. The message to display Prints the new items in a directory upon exiting. The message to display
can include {n} for number of wheels, {s} for total number of seconds, can include {n} for number of wheels, {s} for total number of seconds,
@@ -570,3 +572,16 @@ if sys.version_info >= (3, 8):
from functools import cached_property from functools import cached_property
else: else:
from .functools_cached_property_38 import cached_property from .functools_cached_property_38 import cached_property
# Can be replaced by contextlib.chdir in Python 3.11
@contextlib.contextmanager
def chdir(new_path: Union[Path, str]) -> Generator[None, None, None]:
"""Non thread-safe context manager to change the current working directory."""
cwd = os.getcwd()
try:
os.chdir(new_path)
yield
finally:
os.chdir(cwd)
+1 -1
View File
@@ -14,7 +14,7 @@ The old `manylinux1` image (based on CentOS 5) contains a version of GCC and lib
OS X/macOS allows you to specify a so-called "deployment target" version that will ensure backwards compatibility with older versions of macOS. One way to do this is by setting the `MACOSX_DEPLOYMENT_TARGET` environment variable. OS X/macOS allows you to specify a so-called "deployment target" version that will ensure backwards compatibility with older versions of macOS. One way to do this is by setting the `MACOSX_DEPLOYMENT_TARGET` environment variable.
However, to enable modern C++ standards, the deploment target needs to be set high enough (since older OS X/macOS versions did not have the necessary modern C++ standard library). However, to enable modern C++ standards, the deployment target needs to be set high enough (since older OS X/macOS versions did not have the necessary modern C++ standard library).
To get C++11 and C++14 support, `MACOSX_DEPLOYMENT_TARGET` needs to be set to (at least) `"10.9"`. By default, `cibuildwheel` already does this, building 64-bit-only wheels for macOS 10.9 and later. To get C++11 and C++14 support, `MACOSX_DEPLOYMENT_TARGET` needs to be set to (at least) `"10.9"`. By default, `cibuildwheel` already does this, building 64-bit-only wheels for macOS 10.9 and later.
+1
View File
@@ -13,6 +13,7 @@ extras = {
"pytest>=6", "pytest>=6",
"pytest-timeout", "pytest-timeout",
"pytest-xdist", "pytest-xdist",
"build",
], ],
"bin": [ "bin": [
"click", "click",
+173
View File
@@ -0,0 +1,173 @@
import os
import subprocess
import sys
import textwrap
from pathlib import Path
from tempfile import TemporaryDirectory
from test.test_projects.base import TestProject
from . import test_projects, utils
# utilities
def make_sdist(project: TestProject, working_dir: Path) -> Path:
project_dir = working_dir / "project"
project_dir.mkdir(parents=True, exist_ok=True)
project.generate(project_dir)
sdist_dir = working_dir / "sdist"
subprocess.run(
[sys.executable, "-m", "build", "--sdist", "--outdir", str(sdist_dir), str(project_dir)],
check=True,
)
return next(sdist_dir.glob("*.tar.gz"))
def cibuildwheel_from_sdist_run(sdist_path, add_env=None, config_file=None):
env = os.environ.copy()
if add_env:
env.update(add_env)
with TemporaryDirectory() as tmp_output_dir:
subprocess.run(
[
sys.executable,
"-m",
"cibuildwheel",
*(["--config-file", config_file] if config_file else []),
"--output-dir",
str(tmp_output_dir),
str(sdist_path),
],
env=env,
check=True,
)
return os.listdir(tmp_output_dir)
# tests
def test_simple(tmp_path):
basic_project = test_projects.new_c_project()
# make an sdist of the project
sdist_dir = tmp_path / "sdist"
sdist_dir.mkdir()
sdist_path = make_sdist(basic_project, sdist_dir)
# build the wheels from sdist
actual_wheels = cibuildwheel_from_sdist_run(
sdist_path,
add_env={"CIBW_BUILD": "cp39-*"},
)
# check that the expected wheels are produced
expected_wheels = [w for w in utils.expected_wheels("spam", "0.1.0") if "cp39" in w]
assert set(actual_wheels) == set(expected_wheels)
def test_external_config_file_argument(tmp_path, capfd):
basic_project = test_projects.new_c_project()
# make an sdist of the project
sdist_dir = tmp_path / "sdist"
sdist_dir.mkdir()
sdist_path = make_sdist(basic_project, sdist_dir)
# add a config file
config_file = tmp_path / "config.toml"
config_file.write_text(
textwrap.dedent(
"""
[tool.cibuildwheel]
before-all = 'echo "test log statement from before-all"'
"""
)
)
# build the wheels from sdist
actual_wheels = cibuildwheel_from_sdist_run(
sdist_path,
add_env={"CIBW_BUILD": "cp39-*"},
config_file=str(config_file),
)
# check that the expected wheels are produced
expected_wheels = [w for w in utils.expected_wheels("spam", "0.1.0") if "cp39" in w]
assert set(actual_wheels) == set(expected_wheels)
# check that before-all was run
captured = capfd.readouterr()
assert "test log statement from before-all" in captured.out
def test_config_in_pyproject_toml(tmp_path, capfd):
# make a project with a pyproject.toml
project = test_projects.new_c_project()
project.files["pyproject.toml"] = textwrap.dedent(
"""
[tool.cibuildwheel]
before-build = 'echo "test log statement from before-build 8419"'
"""
)
# make an sdist of the project
sdist_dir = tmp_path / "sdist"
sdist_dir.mkdir()
sdist_path = make_sdist(project, sdist_dir)
# build the wheels from sdist
actual_wheels = cibuildwheel_from_sdist_run(
sdist_path,
add_env={"CIBW_BUILD": "cp39-*"},
)
# check that the expected wheels are produced
expected_wheels = [w for w in utils.expected_wheels("spam", "0.1.0") if "cp39" in w]
assert set(actual_wheels) == set(expected_wheels)
# check that before-build was run
captured = capfd.readouterr()
assert "test log statement from before-build 8419" in captured.out
def test_internal_config_file_argument(tmp_path, capfd):
# make a project with a config file inside
project = test_projects.new_c_project(
setup_cfg_add="include_package_data = True",
)
project.files["wheel_build_config.toml"] = textwrap.dedent(
"""
[tool.cibuildwheel]
before-all = 'echo "test log statement from before-all 1829"'
"""
)
project.files["MANIFEST.in"] = textwrap.dedent(
"""
include wheel_build_config.toml
"""
)
# make an sdist of the project
sdist_dir = tmp_path / "sdist"
sdist_dir.mkdir()
sdist_path = make_sdist(project, sdist_dir)
# build the wheels from sdist, referencing the config file inside
actual_wheels = cibuildwheel_from_sdist_run(
sdist_path,
add_env={"CIBW_BUILD": "cp39-*"},
config_file="{package}/wheel_build_config.toml",
)
# check that the expected wheels are produced
expected_wheels = [w for w in utils.expected_wheels("spam", "0.1.0") if "cp39" in w]
assert set(actual_wheels) == set(expected_wheels)
# check that before-all was run
captured = capfd.readouterr()
assert "test log statement from before-all 1829" in captured.out
+1 -1
View File
@@ -32,7 +32,7 @@ def fake_package_dir(monkeypatch):
real_path_exists = Path.exists real_path_exists = Path.exists
def mock_path_exists(path): def mock_path_exists(path):
if path == MOCK_PACKAGE_DIR / "setup.py": if str(path).endswith(str(MOCK_PACKAGE_DIR / "setup.py")):
return True return True
else: else:
return real_path_exists(path) return real_path_exists(path)
+3 -3
View File
@@ -24,13 +24,13 @@ def test_output_dir(platform, intercepted_build_args, monkeypatch):
main() main()
assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR.resolve()
def test_output_dir_default(platform, intercepted_build_args, monkeypatch): def test_output_dir_default(platform, intercepted_build_args, monkeypatch):
main() main()
assert intercepted_build_args.args[0].globals.output_dir == Path("wheelhouse") assert intercepted_build_args.args[0].globals.output_dir == Path("wheelhouse").resolve()
@pytest.mark.parametrize("also_set_environment", [False, True]) @pytest.mark.parametrize("also_set_environment", [False, True])
@@ -43,7 +43,7 @@ def test_output_dir_argument(also_set_environment, platform, intercepted_build_a
main() main()
assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR.resolve()
def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_empty): def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_empty):
+2 -2
View File
@@ -60,14 +60,14 @@ def test_platform_argument(platform, intercepted_build_args, monkeypatch):
options = intercepted_build_args.args[0] options = intercepted_build_args.args[0]
assert options.globals.package_dir == MOCK_PACKAGE_DIR assert options.globals.package_dir == MOCK_PACKAGE_DIR.resolve()
def test_platform_environment(platform, intercepted_build_args, monkeypatch): def test_platform_environment(platform, intercepted_build_args, monkeypatch):
main() main()
options = intercepted_build_args.args[0] options = intercepted_build_args.args[0]
assert options.globals.package_dir == MOCK_PACKAGE_DIR assert options.globals.package_dir == MOCK_PACKAGE_DIR.resolve()
def test_archs_default(platform, intercepted_build_args, monkeypatch): def test_archs_default(platform, intercepted_build_args, monkeypatch):
+3 -3
View File
@@ -34,7 +34,7 @@ def test_options_1(tmp_path, monkeypatch):
f.write(PYPROJECT_1) f.write(PYPROJECT_1)
args = get_default_command_line_arguments() args = get_default_command_line_arguments()
args.package_dir = str(tmp_path) args.package_dir = tmp_path
monkeypatch.setattr(platform_module, "machine", lambda: "x86_64") monkeypatch.setattr(platform_module, "machine", lambda: "x86_64")
@@ -77,7 +77,7 @@ def test_passthrough(tmp_path, monkeypatch):
f.write(PYPROJECT_1) f.write(PYPROJECT_1)
args = get_default_command_line_arguments() args = get_default_command_line_arguments()
args.package_dir = str(tmp_path) args.package_dir = tmp_path
monkeypatch.setattr(platform_module, "machine", lambda: "x86_64") monkeypatch.setattr(platform_module, "machine", lambda: "x86_64")
monkeypatch.setenv("EXAMPLE_ENV", "ONE") monkeypatch.setenv("EXAMPLE_ENV", "ONE")
@@ -105,7 +105,7 @@ def test_passthrough(tmp_path, monkeypatch):
) )
def test_passthrough_evil(tmp_path, monkeypatch, env_var_value): def test_passthrough_evil(tmp_path, monkeypatch, env_var_value):
args = get_default_command_line_arguments() args = get_default_command_line_arguments()
args.package_dir = str(tmp_path) args.package_dir = tmp_path
monkeypatch.setattr(platform_module, "machine", lambda: "x86_64") monkeypatch.setattr(platform_module, "machine", lambda: "x86_64")
monkeypatch.setenv("CIBW_ENVIRONMENT_PASS_LINUX", "ENV_VAR") monkeypatch.setenv("CIBW_ENVIRONMENT_PASS_LINUX", "ENV_VAR")
+4 -2
View File
@@ -1,3 +1,5 @@
from pathlib import Path
from cibuildwheel.options import CommandLineArguments from cibuildwheel.options import CommandLineArguments
@@ -8,8 +10,8 @@ def get_default_command_line_arguments() -> CommandLineArguments:
defaults.allow_empty = False defaults.allow_empty = False
defaults.archs = None defaults.archs = None
defaults.config_file = "" defaults.config_file = ""
defaults.output_dir = None defaults.output_dir = Path("wheelhouse")
defaults.package_dir = "." defaults.package_dir = Path(".")
defaults.prerelease_pythons = False defaults.prerelease_pythons = False
defaults.print_build_identifiers = False defaults.print_build_identifiers = False