Repository: pypa/cibuildwheel

Issue:
refactor: use run-like syntax
This standardizes the "call" and "shell" commands into a single "run" utility. This utility is just a wrapper around `subprocess.run` that prints the command before running it, and has a better default on Windows due to a Python bug. The goal here is really not to simplify, or to even help out #665 anymore, but to better match the stdlib run syntax to be more recognizable to readers. This is a natural followup to #592 which changed `subprocess.check_call(...)` to `subprocess.run(..., check=True)`. I think if this was developed from scratch today, it would look more like this syntax vs. the old `call`.

Mapping:

* `call(...)` -> `run(..., check=True)`
* `shell(...)` -> `run(..., check=True, shell=True)` (on Windows, for now, this is the same as the above line, but highlights that it is intentionally needing/using the shell, while the one above does not except for the mentioned bug)
* `call_with_arch(...)` -> `run_with_arch(..., check=True)`

I did not change the docker `self.call`, as that's already pretty far from `subprocess.run`, AFAICT, and therefore benefits less from mimicking it. Though it could be changed.

Not absolutely glued to the idea if others have objections. The other direction would be to try to simply the call command to diverge farther from `subprocess.run`, say by accepting `*args`, smarter cwd default, etc. This could then allow `pip = partial(call, "python", "-m", "pip")`, for example.



Discussion:
I'm not sure I agree with the motivation behind this change. I think a key motivator for the existing `call` wrapper is to ensure that we don't accidentally miss `check=True` on a function call. I understand that matching the stdlib might be a bit more familiar, but `check=False` is the wrong default for our use-case, IMO. 

The original call wrapper was a wrapper for `check_call` with an extra printout. Now `run(check=True)` is the recommended method for this instead of `check_call`. What about making `check=` a required keyword argument for the wrapper? That would force it to be clearly included in every usage.
That would help, yeah. But I still don't understand the need to unify the two platforms `run` functions - they're different. The Windows one is quite weird, actually - it calls `run()` with an _array_ of args and `shell=True`. This works because subprocess knows how to escape sequences of params when passing them to a shell on Windows. That isn't the case on Mac, nor does it need to be - the argument list remains an argument list, but just prefixed with `['/bin/sh', '-c']`. So on POSIX, `shell=True` is different - the first argument of `args` becomes special - it's a shell script, not a command. But all other args are passed to it. So on POSIX, `shell=True` completely changes how an argument list is interpreted-

```python
>>> subprocess.run(['/bin/echo', 'hello!'])
hello!
CompletedProcess(args=['/bin/echo', 'hello!'], returncode=0)
>>> subprocess.run(['/bin/echo', 'hello!'], shell=True)

CompletedProcess(args=['/bin/echo', 'hello!'], returncode=0)
```

Note the missing output on the second example. It's missing because the 'hello!' argument went to `/bin/sh` not `/bin/echo`.

(sorry for going deep, but I had to make sure I wasn't making a mountain out of a molehill!)

On windows, this is simulating what _would_ be done if Windows didn't have the (linked) bug. If that bug was fixed, then you could remove the custom handling and it would look like the others. But currently, we have to pretend that `shell=False` even when we have to add `shell=True`. When you pass `shell=True`, you need to pass a string, not a list, and that's what's being done here.
Apologies for the delay in getting back to this @henryiii ! Probably I've been dragging my feet because I'm conflict-averse and I didn't see it as a high priority, but I do see the value in unifying some kind of wrapper, as we saw in #521. So I return to this with slightly more understanding of the why. 

That said,

1. I don't think `check=True` needs to be placed at every call site, when 99% of usages want that. A default of `check=True` makes sense to me (it's kind of crazy that this isn't the default in subprocess... but I digress)
2. I think that executing commands (lists) are different from running shell scripts (strings). This is true in types, but also in  [behaviour](https://github.com/pypa/cibuildwheel/pull/672#issuecomment-846159871). So we should have an API that reflects that. In particular, the `shell=True` flag that allows a list to be passed is problematic, because this has a different behaviour on POSIX versus windows. (this is why the original `call` and `shell` functions are separate)

It would be lovely to match the subprocess.run API for familiarity, but I think that the above concerns are more important, and we wouldn't have a wrapper if we weren't changing behaviour (logging plus the windows workaround), so there is some logic in changing the name.

I've gone around the houses on this, but settled on something quite neat I think. So my counter proposal might look something like:

```python
def call(
    args: Sequence[PathOrStr],
    env: Optional[Dict[str, str]] = None,
    cwd: Optional[PathOrStr] = None,
) -> None:
    """
    Executes a command, like subprocess.run(..., check=True), but:

    - Prints the command before executing it
    - Works around a couple of bugs on Windows
    """

    print("+ " + " ".join(shlex.quote(str(a)) for a in args))

    # execute in a shell on Windows to workaround a PATH resolution bug
    # https://bugs.python.org/issue8557
    shell = sys.platform.startswith("win")

    # converts Paths to strings, due to Windows behavior at least on older Pythons.
    subprocess.run([str(a) for a in args], shell=shell, env=env, cwd=cwd, check=True)
```

- I've used a different name, to deliberately differ from subprocess.run. Not wedded to `call` necessarily, but it makes sense to me and is consistent with existing code and `docker.call`
- This is not a shell interface - it's just for running commands. I realised that we don't use the `shell=True` at all on the linux build, we instead do `call(['sh', '-c', script])`, which is actually better - it's completely explicit, and familiar, and the logs show how the script is invoked. So I'd propose that we do the same on macOS too, replacing `shell()` with `call(['sh', '-c', script])`. I'm not sure if this would be required on windows too, but if needed, there we could also do `call(['cmd', '/c', script])`. This gets us around all the nasty ambiguity of the shell argument having different behaviours and default values on different platforms.

Curious what you think @henryiii. I realise that this doesn't match your original idea, but I think maybe this would unify the API in a way that we can both agree on?

I was always fine going the other way too; I have [not been a fan](https://plumbum.readthedocs.io/en/latest/) of the subprocess syntax since 2015. :)

> The other direction would be to try to simply the call command to diverge farther from subprocess.run, say by accepting *args, smarter cwd default, etc

Since we are coming up with a new syntax, what about:

```python
def call(
    *args: PathOrStr,
    env: Optional[Dict[str, str]] = None,
    cwd: Optional[PathOrStr] = None,
) -> None:
```

Adding the brackets each time isn't very necessary, and it allows this misusage of a plus sign:

```python
call(my_args + ["--other"])
```

to be written like this:

```python
call(*my_args, "--other")
```

Resolving patch:
diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py
--- a/cibuildwheel/macos.py
+++ b/cibuildwheel/macos.py
@@ -1,13 +1,12 @@
 import os
 import platform
 import re
-import shlex
 import shutil
 import subprocess
 import sys
 import tempfile
 from pathlib import Path
-from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Set, Tuple, cast
+from typing import Any, Dict, List, NamedTuple, Sequence, Set, Tuple, cast
 
 from .architecture import Architecture
 from .environment import ParsedEnvironment
@@ -18,31 +17,18 @@
     BuildFrontend,
     BuildSelector,
     NonPlatformWheelError,
+    call,
     download,
     get_build_verbosity_extra_flags,
     get_pip_version,
     install_certifi_script,
     prepare_command,
     read_python_configs,
+    shell,
     unwrap,
 )
 
 
-def call(
-    args: Sequence[PathOrStr],
-    env: Optional[Dict[str, str]] = None,
-    cwd: Optional[str] = None,
-    shell: bool = False,
-) -> None:
-    # print the command executing for the logs
-    if shell:
-        print(f"+ {args}")
-    else:
-        print("+ " + " ".join(shlex.quote(str(a)) for a in args))
-
-    subprocess.run(args, env=env, cwd=cwd, shell=shell, check=True)
-
-
 def get_macos_version() -> Tuple[int, int]:
     """
     Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0)
@@ -58,13 +44,7 @@ def get_macos_version() -> Tuple[int, int]:
 
 
 def get_macos_sdks() -> List[str]:
-    output = subprocess.run(
-        ["xcodebuild", "-showsdks"],
-        universal_newlines=True,
-        check=True,
-        stdout=subprocess.PIPE,
-    ).stdout
-
+    output = call("xcodebuild", "-showsdks", capture_stdout=True)
     return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
 
 
@@ -114,9 +94,7 @@ def make_symlinks(installation_bin_path: Path, python_executable: str, pip_execu
 
 
 def install_cpython(version: str, url: str) -> Path:
-    installed_system_packages = subprocess.run(
-        ["pkgutil", "--pkgs"], universal_newlines=True, check=True, stdout=subprocess.PIPE
-    ).stdout.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
     python_package_identifier = f"org.python.Python.PythonFramework-{version}"
@@ -127,10 +105,10 @@ def install_cpython(version: str, url: str) -> Path:
         # download the pkg
         download(url, Path("/tmp/Python.pkg"))
         # install
-        call(["sudo", "installer", "-pkg", "/tmp/Python.pkg", "-target", "/"])
+        call("sudo", "installer", "-pkg", "/tmp/Python.pkg", "-target", "/")
         env = os.environ.copy()
         env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
-        call([str(installation_bin_path / python_executable), str(install_certifi_script)], env=env)
+        call(str(installation_bin_path / python_executable), str(install_certifi_script), env=env)
 
     pip_executable = "pip3"
     make_symlinks(installation_bin_path, python_executable, pip_executable)
@@ -147,7 +125,7 @@ def install_pypy(version: str, url: str) -> Path:
     if not installation_path.exists():
         downloaded_tar_bz2 = Path("/tmp") / pypy_tar_bz2
         download(url, downloaded_tar_bz2)
-        call(["tar", "-C", "/tmp", "-xf", downloaded_tar_bz2])
+        call("tar", "-C", "/tmp", "-xf", downloaded_tar_bz2)
 
     installation_bin_path = installation_path / "bin"
     python_executable = "pypy3"
@@ -203,20 +181,18 @@ def setup_python(
     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")
+        call("python", "-m", "ensurepip", env=env, cwd="/tmp")
 
     # upgrade pip to the version matching our constraints
     # if necessary, reinstall it to ensure that it's available on PATH as 'pip'
     call(
-        [
-            "python",
-            "-m",
-            "pip",
-            "install",
-            "--force-reinstall" if requires_reinstall else "--upgrade",
-            "pip",
-            *dependency_constraint_flags,
-        ],
+        "python",
+        "-m",
+        "pip",
+        "install",
+        "--force-reinstall" if requires_reinstall else "--upgrade",
+        "pip",
+        *dependency_constraint_flags,
         env=env,
         cwd="/tmp",
     )
@@ -226,11 +202,9 @@ def setup_python(
 
     # check what pip version we're on
     assert (installation_bin_path / "pip").exists()
-    call(["which", "pip"], env=env)
-    call(["pip", "--version"], env=env)
-    which_pip = subprocess.run(
-        ["which", "pip"], env=env, universal_newlines=True, check=True, stdout=subprocess.PIPE
-    ).stdout.strip()
+    call("which", "pip", env=env)
+    call("pip", "--version", env=env)
+    which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
     if which_pip != "/tmp/cibw_bin/pip":
         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.",
@@ -239,11 +213,9 @@ def setup_python(
         sys.exit(1)
 
     # check what Python version we're on
-    call(["which", "python"], env=env)
-    call(["python", "--version"], env=env)
-    which_python = subprocess.run(
-        ["which", "python"], env=env, universal_newlines=True, check=True, stdout=subprocess.PIPE
-    ).stdout.strip()
+    call("which", "python", env=env)
+    call("python", "--version", env=env)
+    which_python = call("which", "python", env=env, capture_stdout=True).strip()
     if which_python != "/tmp/cibw_bin/python":
         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.",
@@ -298,27 +270,23 @@ def setup_python(
     log.step("Installing build tools...")
     if build_frontend == "pip":
         call(
-            [
-                "pip",
-                "install",
-                "--upgrade",
-                "setuptools",
-                "wheel",
-                "delocate",
-                *dependency_constraint_flags,
-            ],
+            "pip",
+            "install",
+            "--upgrade",
+            "setuptools",
+            "wheel",
+            "delocate",
+            *dependency_constraint_flags,
             env=env,
         )
     elif build_frontend == "build":
         call(
-            [
-                "pip",
-                "install",
-                "--upgrade",
-                "delocate",
-                "build[virtualenv]",
-                *dependency_constraint_flags,
-            ],
+            "pip",
+            "install",
+            "--upgrade",
+            "delocate",
+            "build[virtualenv]",
+            *dependency_constraint_flags,
             env=env,
         )
     else:
@@ -347,7 +315,7 @@ def build(options: Options) -> None:
             before_all_prepared = prepare_command(
                 before_all_options.before_all, project=".", package=before_all_options.package_dir
             )
-            call([before_all_prepared], shell=True, env=env)
+            shell(before_all_prepared, env=env)
 
         for config in python_configurations:
             build_options = options.build_options(config.identifier)
@@ -375,7 +343,7 @@ def build(options: Options) -> None:
                 before_build_prepared = prepare_command(
                     build_options.before_build, project=".", package=build_options.package_dir
                 )
-                call(before_build_prepared, env=env, shell=True)
+                shell(before_build_prepared, env=env)
 
             log.step("Building wheel...")
             if built_wheel_dir.exists():
@@ -388,16 +356,14 @@ def build(options: Options) -> None:
                 # Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
                 # see https://github.com/pypa/cibuildwheel/pull/369
                 call(
-                    [
-                        "python",
-                        "-m",
-                        "pip",
-                        "wheel",
-                        build_options.package_dir.resolve(),
-                        f"--wheel-dir={built_wheel_dir}",
-                        "--no-deps",
-                        *verbosity_flags,
-                    ],
+                    "python",
+                    "-m",
+                    "pip",
+                    "wheel",
+                    build_options.package_dir.resolve(),
+                    f"--wheel-dir={built_wheel_dir}",
+                    "--no-deps",
+                    *verbosity_flags,
                     env=env,
                 )
             elif build_options.build_frontend == "build":
@@ -410,15 +376,13 @@ def build(options: Options) -> None:
                     build_env["PIP_CONSTRAINT"] = constraint_path.as_uri()
                 build_env["VIRTUALENV_PIP"] = get_pip_version(env)
                 call(
-                    [
-                        "python",
-                        "-m",
-                        "build",
-                        build_options.package_dir,
-                        "--wheel",
-                        f"--outdir={built_wheel_dir}",
-                        f"--config-setting={config_setting}",
-                    ],
+                    "python",
+                    "-m",
+                    "build",
+                    build_options.package_dir,
+                    "--wheel",
+                    f"--outdir={built_wheel_dir}",
+                    f"--config-setting={config_setting}",
                     env=build_env,
                 )
             else:
@@ -449,7 +413,7 @@ def build(options: Options) -> None:
                     dest_dir=repaired_wheel_dir,
                     delocate_archs=delocate_archs,
                 )
-                call(repair_command_prepared, env=env, shell=True)
+                shell(repair_command_prepared, env=env)
             else:
                 shutil.move(str(built_wheel), repaired_wheel_dir)
 
@@ -514,7 +478,7 @@ def build(options: Options) -> None:
 
                     # set up a virtual environment to install and test from, to make sure
                     # 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())
 
                     arch_prefix = []
@@ -528,18 +492,16 @@ def build(options: Options) -> None:
                             )
 
                     # define a custom 'call' function that adds the arch prefix each time
-                    def call_with_arch(args: Sequence[PathOrStr], **kwargs: Any) -> None:
-                        if isinstance(args, str):
-                            args = " ".join(arch_prefix) + " " + args
-                        else:
-                            args = [*arch_prefix, *args]
-                        call(args, **kwargs)
+                    def call_with_arch(*args: PathOrStr, **kwargs: Any) -> None:
+                        call(*arch_prefix, *args, **kwargs)
+
+                    def shell_with_arch(command: str, **kwargs: Any) -> None:
+                        command = " ".join(arch_prefix) + " " + command
+                        shell(command, **kwargs)
 
                     # Use --no-download to ensure determinism by using seed libraries
                     # built into virtualenv
-                    call_with_arch(
-                        ["python", "-m", "virtualenv", "--no-download", venv_dir], env=env
-                    )
+                    call_with_arch("python", "-m", "virtualenv", "--no-download", venv_dir, env=env)
 
                     virtualenv_env = env.copy()
                     virtualenv_env["PATH"] = os.pathsep.join(
@@ -550,7 +512,7 @@ def call_with_arch(args: Sequence[PathOrStr], **kwargs: Any) -> None:
                     )
 
                     # check that we are using the Python from the virtual environment
-                    call_with_arch(["which", "python"], env=virtualenv_env)
+                    call_with_arch("which", "python", env=virtualenv_env)
 
                     if build_options.before_test:
                         before_test_prepared = prepare_command(
@@ -558,18 +520,20 @@ def call_with_arch(args: Sequence[PathOrStr], **kwargs: Any) -> None:
                             project=".",
                             package=build_options.package_dir,
                         )
-                        call_with_arch(before_test_prepared, env=virtualenv_env, shell=True)
+                        shell_with_arch(before_test_prepared, env=virtualenv_env)
 
                     # install the wheel
                     call_with_arch(
-                        ["pip", "install", f"{repaired_wheel}{build_options.test_extras}"],
+                        "pip",
+                        "install",
+                        f"{repaired_wheel}{build_options.test_extras}",
                         env=virtualenv_env,
                     )
 
                     # test the wheel
                     if build_options.test_requires:
                         call_with_arch(
-                            ["pip", "install"] + build_options.test_requires, env=virtualenv_env
+                            "pip", "install", *build_options.test_requires, env=virtualenv_env
                         )
 
                     # run the tests from $HOME, with an absolute path in the command
@@ -580,11 +544,8 @@ def call_with_arch(args: Sequence[PathOrStr], **kwargs: Any) -> None:
                         project=Path(".").resolve(),
                         package=build_options.package_dir.resolve(),
                     )
-                    call_with_arch(
-                        test_command_prepared,
-                        cwd=os.environ["HOME"],
-                        env=virtualenv_env,
-                        shell=True,
+                    shell_with_arch(
+                        test_command_prepared, cwd=os.environ["HOME"], env=virtualenv_env
                     )
 
                     # clean up
diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py
--- a/cibuildwheel/util.py
+++ b/cibuildwheel/util.py
@@ -3,6 +3,7 @@
 import itertools
 import os
 import re
+import shlex
 import ssl
 import subprocess
 import sys
@@ -12,7 +13,18 @@
 from enum import Enum
 from pathlib import Path
 from time import sleep
-from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, TextIO
+from typing import (
+    Any,
+    Dict,
+    Iterable,
+    Iterator,
+    List,
+    NamedTuple,
+    Optional,
+    TextIO,
+    cast,
+    overload,
+)
 
 import bracex
 import certifi
@@ -47,6 +59,60 @@
     "s390x",
 )
 
+IS_WIN = sys.platform.startswith("win")
+
+
+@overload
+def call(
+    *args: PathOrStr,
+    env: Optional[Dict[str, str]] = None,
+    cwd: Optional[PathOrStr] = None,
+    capture_stdout: Literal[False] = ...,
+) -> None:
+    ...
+
+
+@overload
+def call(
+    *args: PathOrStr,
+    env: Optional[Dict[str, str]] = None,
+    cwd: Optional[PathOrStr] = None,
+    capture_stdout: Literal[True],
+) -> str:
+    ...
+
+
+def call(
+    *args: PathOrStr,
+    env: Optional[Dict[str, str]] = None,
+    cwd: Optional[PathOrStr] = None,
+    capture_stdout: bool = False,
+) -> Optional[str]:
+    """
+    Run subprocess.run, but print the commands first. Takes the commands as
+    *args. Uses shell=True on Windows due to a bug. Also converts to
+    Paths to strings, due to Windows behavior at least on older Pythons.
+    https://bugs.python.org/issue8557
+    """
+    args_ = [str(arg) for arg in args]
+    # print the command executing for the logs
+    print("+ " + " ".join(shlex.quote(a) for a in args_))
+    kwargs: Dict[str, Any] = {}
+    if capture_stdout:
+        kwargs["universal_newlines"] = True
+        kwargs["stdout"] = subprocess.PIPE
+    result = subprocess.run(args_, check=True, shell=IS_WIN, env=env, cwd=cwd, **kwargs)
+    if not capture_stdout:
+        return None
+    return cast(str, result.stdout)
+
+
+def shell(
+    command: str, env: Optional[Dict[str, str]] = None, cwd: Optional[PathOrStr] = None
+) -> None:
+    print(f"+ {command}")
+    subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
+
 
 def format_safe(template: str, **kwargs: Any) -> str:
     """
@@ -376,11 +442,8 @@ class FileReport(NamedTuple):
 
 
 def get_pip_version(env: Dict[str, str]) -> str:
-    # we use shell=True here for windows, even though we don't need a shell due to a bug
-    # https://bugs.python.org/issue8557
-    shell = sys.platform.startswith("win")
-    versions_output_text = subprocess.check_output(
-        ["python", "-m", "pip", "freeze", "--all"], universal_newlines=True, shell=shell, env=env
+    versions_output_text = call(
+        "python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
     )
     (pip_version,) = (
         version[5:]
diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py
--- a/cibuildwheel/windows.py
+++ b/cibuildwheel/windows.py
@@ -18,32 +18,18 @@
     BuildFrontend,
     BuildSelector,
     NonPlatformWheelError,
+    call,
     download,
     get_build_verbosity_extra_flags,
     get_pip_version,
     prepare_command,
     read_python_configs,
+    shell,
 )
 
 CIBW_INSTALL_PATH = Path("C:\\cibw")
 
 
-def call(
-    args: Sequence[PathOrStr], env: Optional[Dict[str, str]] = None, cwd: Optional[PathOrStr] = None
-) -> None:
-    print("+ " + " ".join(str(a) for a in args))
-    # we use shell=True here, even though we don't need a shell due to a bug
-    # https://bugs.python.org/issue8557
-    subprocess.run([str(a) for a in args], env=env, cwd=cwd, shell=True, check=True)
-
-
-def shell(
-    command: str, env: Optional[Dict[str, str]] = None, cwd: Optional[PathOrStr] = None
-) -> None:
-    print(f"+ {command}")
-    subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
-
-
 def get_nuget_args(version: str, arch: str) -> List[str]:
     platform_suffix = {"32": "x86", "64": "", "ARM64": "arm64"}
     python_name = "python" + platform_suffix[arch]
@@ -94,7 +80,7 @@ def extract_zip(zip_src: Path, dest: Path) -> None:
 def install_cpython(version: str, arch: str, nuget: Path) -> Path:
     nuget_args = get_nuget_args(version, arch)
     installation_path = Path(nuget_args[-1]) / (nuget_args[0] + "." + version) / "tools"
-    call([nuget, "install", *nuget_args])
+    call(nuget, "install", *nuget_args)
     # "python3" is not included in the vanilla nuget package,
     # though it can be present if modified (like on Azure).
     if not (installation_path / "python3.exe").exists():
@@ -165,7 +151,7 @@ def setup_python(
 
     if requires_reinstall:
         # maybe pip isn't installed at all. ensurepip resolves that.
-        call(["python", "-m", "ensurepip"], env=env, cwd=CIBW_INSTALL_PATH)
+        call("python", "-m", "ensurepip", env=env, cwd=CIBW_INSTALL_PATH)
 
     # 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
@@ -175,16 +161,14 @@ def setup_python(
     # pip versions newer than 21.3.
     if python_configuration.arch == "ARM64" and Version(get_pip_version(env)) < Version("21.3"):
         call(
-            [
-                "python",
-                "-m",
-                "pip",
-                "install",
-                "--force-reinstall",
-                "--upgrade",
-                "pip",
-                *dependency_constraint_flags,
-            ],
+            "python",
+            "-m",
+            "pip",
+            "install",
+            "--force-reinstall",
+            "--upgrade",
+            "pip",
+            *dependency_constraint_flags,
             env=env,
             cwd=CIBW_INSTALL_PATH,
         )
@@ -192,15 +176,13 @@ def setup_python(
     # upgrade pip to the version matching our constraints
     # if necessary, reinstall it to ensure that it's available on PATH as 'pip.exe'
     call(
-        [
-            "python",
-            "-m",
-            "pip",
-            "install",
-            "--force-reinstall" if requires_reinstall else "--upgrade",
-            "pip",
-            *dependency_constraint_flags,
-        ],
+        "python",
+        "-m",
+        "pip",
+        "install",
+        "--force-reinstall" if requires_reinstall else "--upgrade",
+        "pip",
+        *dependency_constraint_flags,
         env=env,
         cwd=CIBW_INSTALL_PATH,
     )
@@ -209,20 +191,10 @@ def setup_python(
     env = environment.as_dictionary(prev_environment=env)
 
     # check what Python version we're on
-    call(["where", "python"], env=env)
-    call(["python", "--version"], env=env)
-    call(["python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\""], env=env)
-    where_python = (
-        subprocess.run(
-            ["where", "python"],
-            env=env,
-            universal_newlines=True,
-            check=True,
-            stdout=subprocess.PIPE,
-        )
-        .stdout.splitlines()[0]
-        .strip()
-    )
+    call("where", "python", env=env)
+    call("python", "--version", 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()
     if where_python != str(installation_path / "python.exe"):
         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.",
@@ -232,13 +204,7 @@ def setup_python(
 
     # check what pip version we're on
     assert (installation_path / "Scripts" / "pip.exe").exists()
-    where_pip = (
-        subprocess.run(
-            ["where", "pip"], env=env, universal_newlines=True, check=True, stdout=subprocess.PIPE
-        )
-        .stdout.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"):
         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.",
@@ -246,23 +212,25 @@ def setup_python(
         )
         sys.exit(1)
 
-    call(["pip", "--version"], env=env)
+    call("pip", "--version", env=env)
 
     if build_frontend == "pip":
         call(
-            [
-                "pip",
-                "install",
-                "--upgrade",
-                "setuptools",
-                "wheel",
-                *dependency_constraint_flags,
-            ],
+            "pip",
+            "install",
+            "--upgrade",
+            "setuptools",
+            "wheel",
+            *dependency_constraint_flags,
             env=env,
         )
     elif build_frontend == "build":
         call(
-            ["pip", "install", "--upgrade", "build[virtualenv]", *dependency_constraint_flags],
+            "pip",
+            "install",
+            "--upgrade",
+            "build[virtualenv]",
+            *dependency_constraint_flags,
             env=env,
         )
     else:
@@ -330,16 +298,14 @@ def build(options: Options) -> None:
                 # Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
                 # see https://github.com/pypa/cibuildwheel/pull/369
                 call(
-                    [
-                        "python",
-                        "-m",
-                        "pip",
-                        "wheel",
-                        options.globals.package_dir.resolve(),
-                        f"--wheel-dir={built_wheel_dir}",
-                        "--no-deps",
-                        *get_build_verbosity_extra_flags(build_options.build_verbosity),
-                    ],
+                    "python",
+                    "-m",
+                    "pip",
+                    "wheel",
+                    options.globals.package_dir.resolve(),
+                    f"--wheel-dir={built_wheel_dir}",
+                    "--no-deps",
+                    *get_build_verbosity_extra_flags(build_options.build_verbosity),
                     env=env,
                 )
             elif build_options.build_frontend == "build":
@@ -364,15 +330,13 @@ def build(options: Options) -> None:
                     build_env["PIP_CONSTRAINT"] = str(constraints_path)
                     build_env["VIRTUALENV_PIP"] = get_pip_version(env)
                     call(
-                        [
-                            "python",
-                            "-m",
-                            "build",
-                            build_options.package_dir,
-                            "--wheel",
-                            f"--outdir={built_wheel_dir}",
-                            f"--config-setting={config_setting}",
-                        ],
+                        "python",
+                        "-m",
+                        "build",
+                        build_options.package_dir,
+                        "--wheel",
+                        f"--outdir={built_wheel_dir}",
+                        f"--config-setting={config_setting}",
                         env=build_env,
                     )
             else:
@@ -403,12 +367,12 @@ def build(options: Options) -> None:
                 log.step("Testing wheel...")
                 # set up a virtual environment to install and test from, to make sure
                 # 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())
 
                 # Use --no-download to ensure determinism by using seed libraries
                 # built into virtualenv
-                call(["python", "-m", "virtualenv", "--no-download", venv_dir], env=env)
+                call("python", "-m", "virtualenv", "--no-download", venv_dir, env=env)
 
                 virtualenv_env = env.copy()
                 virtualenv_env["PATH"] = os.pathsep.join(
@@ -419,7 +383,7 @@ def build(options: Options) -> None:
                 )
 
                 # check that we are using the Python from the virtual environment
-                call(["where", "python"], env=virtualenv_env)
+                call("where", "python", env=virtualenv_env)
 
                 if build_options.before_test:
                     before_test_prepared = prepare_command(
@@ -431,13 +395,15 @@ def build(options: Options) -> None:
 
                 # install the wheel
                 call(
-                    ["pip", "install", str(repaired_wheel) + build_options.test_extras],
+                    "pip",
+                    "install",
+                    str(repaired_wheel) + build_options.test_extras,
                     env=virtualenv_env,
                 )
 
                 # test the wheel
                 if build_options.test_requires:
-                    call(["pip", "install"] + build_options.test_requires, env=virtualenv_env)
+                    call("pip", "install", *build_options.test_requires, env=virtualenv_env)
 
                 # run the tests from c:\, with an absolute path in the command
                 # (this ensures that Python runs the tests against the installed wheel


Test patch: