diff --git a/bin/generate_schema.py b/bin/generate_schema.py index e4f4507d2..e780db4b7 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -61,9 +61,9 @@ oneOf: - enum: [docker, podman] - type: string - pattern: '^docker; ?create_args:' + pattern: '^docker; ?(create_args|disable_host_mount):' - type: string - pattern: '^podman; ?create_args:' + pattern: '^podman; ?(create_args|disable_host_mount):' - type: object additionalProperties: false required: [name] @@ -74,6 +74,8 @@ type: array items: type: string + disable-host-mount: + type: boolean dependency-versions: default: pinned description: Specify how cibuildwheel controls the versions of the tools it uses diff --git a/cibuildwheel/oci_container.py b/cibuildwheel/oci_container.py index 1b2e207bf..7fceca9e9 100644 --- a/cibuildwheel/oci_container.py +++ b/cibuildwheel/oci_container.py @@ -32,11 +32,14 @@ class OCIContainerEngineConfig: name: ContainerEngineName create_args: Sequence[str] = () + disable_host_mount: bool = False @staticmethod def from_config_string(config_string: str) -> OCIContainerEngineConfig: config_dict = parse_key_value_string( - config_string, ["name"], ["create_args", "create-args"] + config_string, + ["name"], + ["create_args", "create-args", "disable_host_mount", "disable-host-mount"], ) name = " ".join(config_dict["name"]) if name not in {"docker", "podman"}: @@ -44,15 +47,28 @@ def from_config_string(config_string: str) -> OCIContainerEngineConfig: raise ValueError(msg) name = typing.cast(ContainerEngineName, name) - # some flexibility in the option name to cope with TOML conventions + # some flexibility in the option names to cope with TOML conventions create_args = config_dict.get("create_args") or config_dict.get("create-args") or [] - return OCIContainerEngineConfig(name=name, create_args=create_args) + disable_host_mount_options = ( + config_dict.get("disable_host_mount") or config_dict.get("disable-host-mount") or [] + ) + disable_host_mount = ( + strtobool(disable_host_mount_options[-1]) if disable_host_mount_options else False + ) + + return OCIContainerEngineConfig( + name=name, create_args=create_args, disable_host_mount=disable_host_mount + ) def options_summary(self) -> str | dict[str, str]: if not self.create_args: return self.name else: - return {"name": self.name, "create_args": repr(self.create_args)} + return { + "name": self.name, + "create_args": repr(self.create_args), + "disable_host_mount": str(self.disable_host_mount), + } DEFAULT_ENGINE = OCIContainerEngineConfig("docker") @@ -136,7 +152,7 @@ def __enter__(self) -> OCIContainer: "--env=SOURCE_DATE_EPOCH", f"--name={self.name}", "--interactive", - "--volume=/:/host", # ignored on CircleCI + *(["--volume=/:/host"] if not self.engine.disable_host_mount else []), *network_args, *self.engine.create_args, self.image, diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index f5664cbf1..418b19f52 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -378,6 +378,9 @@ def _inner_fmt(k: str, v: Any, table: TableFmt) -> Iterator[str]: for inner_v in v: qv = quote_function(inner_v) yield table["item"].format(k=k, v=qv) + elif isinstance(v, bool): + qv = quote_function(str(v)) + yield table["item"].format(k=k, v=qv) else: qv = quote_function(v) yield table["item"].format(k=k, v=qv) diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index 99b11117f..df4af6a90 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -172,11 +172,11 @@ }, { "type": "string", - "pattern": "^docker; ?create_args:" + "pattern": "^docker; ?(create_args|disable_host_mount):" }, { "type": "string", - "pattern": "^podman; ?create_args:" + "pattern": "^podman; ?(create_args|disable_host_mount):" }, { "type": "object", @@ -196,6 +196,9 @@ "items": { "type": "string" } + }, + "disable-host-mount": { + "type": "boolean" } } } diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index 0ca16b041..aa523de8f 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -741,7 +741,7 @@ def parse_key_value_string( # split by semicolon fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k] - result: dict[str, list[str]] = defaultdict(list) + result: defaultdict[str, list[str]] = defaultdict(list) for field_i, field in enumerate(fields): # check to see if the option name is specified field_name, sep, first_value = field[0].partition(":") @@ -762,4 +762,4 @@ def parse_key_value_string( result[field_name] += values - return result + return dict(result) diff --git a/docs/extra.css b/docs/extra.css index eac0a1934..232ca2c72 100644 --- a/docs/extra.css +++ b/docs/extra.css @@ -315,3 +315,9 @@ h1, h2, h3, h4, h5, h6 { .wy-menu-vertical li.current a { } + +/* word wrap in table cells */ +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal; + line-height: 1.4; +} diff --git a/docs/faq.md b/docs/faq.md index 1dfe41080..3b159cea6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -14,7 +14,7 @@ Linux wheels are built in [`manylinux`/`musllinux` containers](https://github.co `cibuildwheel` supports this by providing the [`CIBW_ENVIRONMENT`](options.md#environment) and [`CIBW_BEFORE_ALL`](options.md#before-all) options to setup the build environment inside the running container. -- The project directory is copied into the container as `/project`, the output directory for the wheels to be copied out is `/output`. In general, this is handled transparently by `cibuildwheel`. For a more finegrained level of control however, the root of the host file system is mounted as `/host`, allowing for example to access shared files, caches, etc. on the host file system. Note that `/host` is not available on CircleCI due to their Docker policies. +- The project directory is copied into the container as `/project`, the output directory for the wheels to be copied out is `/output`. In general, this is handled transparently by `cibuildwheel`. For a more finegrained level of control however, the root of the host file system is mounted as `/host`, allowing for example to access shared files, caches, etc. on the host file system. Note that `/host` is not available on CircleCI and GitLab CI due to their Docker policies. - Alternative Docker images can be specified with the `CIBW_MANYLINUX_*_IMAGE`/`CIBW_MUSLLINUX_*_IMAGE` options to allow for a custom, preconfigured build environment for the Linux builds. See [options](options.md#linux-image) for more details. diff --git a/docs/options.md b/docs/options.md index 513cfc21a..3acf82b09 100644 --- a/docs/options.md +++ b/docs/options.md @@ -1069,8 +1069,8 @@ Auditwheel detects the version of the manylinux / musllinux standard in the imag Options: -- `docker[;create_args: ...]` -- `podman[;create_args: ...]` +- `docker[;create_args: ...][;disable_host_mount: true/false]` +- `podman[;create_args: ...][;disable_host_mount: true/false]` Default: `docker` @@ -1079,11 +1079,13 @@ Set the container engine to use. Docker is the default, or you can switch to running and `docker` available on PATH. To use Podman, it needs to be installed and `podman` available on PATH. -Arguments can be supplied to the container engine. Currently, the only option -that's customisable is 'create_args'. Parameters to create_args are -space-separated strings, which are passed to the container engine on the -command line when it's creating the container. If you want to include spaces -inside a parameter, use shell-style quoting. +Options can be supplied after the name. + +| Option name | Description +|---|--- +| `create_args` | Space-separated strings, which are passed to the container engine on the command line when it's creating the container. If you want to include spaces inside a parameter, use shell-style quoting. +| `disable_host_mount` | By default, cibuildwheel will mount the root of the host filesystem as a volume at `/host` in the container. To disable the host mount, pass `true` to this option. + !!! tip @@ -1104,6 +1106,9 @@ inside a parameter, use shell-style quoting. # pass command line options to 'docker create' CIBW_CONTAINER_ENGINE: "docker; create_args: --gpus all" + + # disable the /host mount + CIBW_CONTAINER_ENGINE: "docker; disable_host_mount: true" ``` !!! tab examples "pyproject.toml" @@ -1115,6 +1120,9 @@ inside a parameter, use shell-style quoting. # pass command line options to 'docker create' container-engine = { name = "docker", create-args = ["--gpus", "all"]} + + # disable the /host mount + container-engine = { name = "docker", disable-host-mount = true } ``` diff --git a/unit_test/oci_container_test.py b/unit_test/oci_container_test.py index c342892d1..161f8cfab 100644 --- a/unit_test/oci_container_test.py +++ b/unit_test/oci_container_test.py @@ -14,7 +14,7 @@ from cibuildwheel.environment import EnvironmentAssignmentBash from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig -from cibuildwheel.util import detect_ci_provider +from cibuildwheel.util import CIProvider, detect_ci_provider # Test utilities @@ -415,3 +415,29 @@ def test_enforce_32_bit(container_engine, image, shell_args): text=True, ).stdout assert json.loads(container_args) == shell_args + + +@pytest.mark.parametrize( + ("config", "should_have_host_mount"), + [ + ("{name}", True), + ("{name}; disable_host_mount: false", True), + ("{name}; disable_host_mount: true", False), + ], +) +def test_disable_host_mount(tmp_path: Path, container_engine, config, should_have_host_mount): + if detect_ci_provider() in {CIProvider.circle_ci, CIProvider.gitlab}: + pytest.skip("Skipping test because docker on this platform does not support host mounts") + + engine = OCIContainerEngineConfig.from_config_string(config.format(name=container_engine.name)) + + sentinel_file = tmp_path / "sentinel" + sentinel_file.write_text("12345") + + with OCIContainer(engine=engine, image=DEFAULT_IMAGE) as container: + host_mount_path = "/host" + str(sentinel_file) + if should_have_host_mount: + assert container.call(["cat", host_mount_path], capture_output=True) == "12345" + else: + with pytest.raises(subprocess.CalledProcessError): + container.call(["cat", host_mount_path], capture_output=True) diff --git a/unit_test/options_test.py b/unit_test/options_test.py index 3a823d6c4..c2163090f 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -201,41 +201,61 @@ def test_toml_environment_quoting(tmp_path: Path, toml_assignment, result_value) @pytest.mark.parametrize( - ("toml_assignment", "result_name", "result_create_args"), + ("toml_assignment", "result_name", "result_create_args", "result_disable_host_mount"), [ ( 'container-engine = "podman"', "podman", [], + False, ), ( 'container-engine = {name = "podman"}', "podman", [], + False, ), ( 'container-engine = "docker; create_args: --some-option"', "docker", ["--some-option"], + False, ), ( 'container-engine = {name = "docker", create-args = ["--some-option"]}', "docker", ["--some-option"], + False, ), ( 'container-engine = {name = "docker", create-args = ["--some-option", "value that contains spaces"]}', "docker", ["--some-option", "value that contains spaces"], + False, ), ( 'container-engine = {name = "docker", create-args = ["--some-option", "value;that;contains;semicolons"]}', "docker", ["--some-option", "value;that;contains;semicolons"], + False, + ), + ( + 'container-engine = {name = "docker", disable-host-mount = true}', + "docker", + [], + True, + ), + ( + 'container-engine = {name = "docker", disable_host_mount = true}', + "docker", + [], + True, ), ], ) -def test_container_engine_option(tmp_path: Path, toml_assignment, result_name, result_create_args): +def test_container_engine_option( + tmp_path: Path, toml_assignment, result_name, result_create_args, result_disable_host_mount +): args = CommandLineArguments.defaults() args.package_dir = tmp_path @@ -253,6 +273,7 @@ def test_container_engine_option(tmp_path: Path, toml_assignment, result_name, r assert parsed_container_engine.name == result_name assert parsed_container_engine.create_args == result_create_args + assert parsed_container_engine.disable_host_mount == result_disable_host_mount def test_environment_pass_references():