Repository: pypa/cibuildwheel

Issue:
bug: space in directory name can cause issues
When testing out build support in cibuildwheel 2.0.0a4, I run into this failure:

```pytb
python -m build . --wheel --outdir=C:\Users\RUNNER~1\AppData\Local\Temp\cibuildwheelt0jax362\built_wheel --config-setting=-v
ERROR: Could not open requirements file: [Errno 2] No such file or directory: 'C:\\Program'
Traceback (most recent call last):
  File "C:\cibw\python\python.3.8.10\tools\lib\site-packages\build\__main__.py", line 302, in main
    build_call(args.srcdir, outdir, distributions, config_settings, not args.no_isolation, args.skip_dependency_check)
  File "C:\cibw\python\python.3.8.10\tools\lib\site-packages\build\__main__.py", line 145, in build_package
    _build(isolation, builder, outdir, distribution, config_settings, skip_dependency_check)
  File "C:\cibw\python\python.3.8.10\tools\lib\site-packages\build\__main__.py", line 101, in _build
    return _build_in_isolated_env(builder, outdir, distribution, config_settings)
  File "C:\cibw\python\python.3.8.10\tools\lib\site-packages\build\__main__.py", line 81, in _build_in_isolated_env
    env.install(builder.build_system_requires)
  File "C:\cibw\python\python.3.8.10\tools\lib\site-packages\build\env.py", line 169, in install
    subprocess.check_call(cmd)
  File "C:\cibw\python\python.3.8.10\tools\lib\subprocess.py", line 364, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['C:\\Users\\runneradmin\\AppData\\Local\\Temp\\build-env-s3f7jwu0\\Scripts\\python.exe', '-Im', 'pip', 'install', '--use-pep517', '--no-warn-script-location', '-r', 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\build-reqs-ckde1r2k.txt']' returned non-zero exit status 1.

ERROR Command '['C:\\Users\\runneradmin\\AppData\\Local\\Temp\\build-env-s3f7jwu0\\Scripts\\python.exe', '-Im', 'pip', 'install', '--use-pep517', '--no-warn-script-location', '-r', 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\build-reqs-ckde1r2k.txt']' returned non-zero exit status 1.
```

I _think_ this is a problem with quoting paths on Windows probably in `build`, or maybe `pip`? The same build works using `pip wheel` from cibuildwheel. Seen in scikit-hep/boost-histogram#583.


Discussion:
Where is the "Program" (presumably "Program Files") path coming from?
I haven't figured that out yet. The error is coming from pip: https://github.com/pypa/pip/blob/7a77484a492c8f1e1f5ef24eaf71a43df9ea47eb/src/pip/_internal/req/req_file.py#L549 
`cibuildwheel` installs Python into the "normal" location for macOS and Windows, so that's likely in Program Files. But have no idea why there's a requirement file there. (I'll try to start up a Windows machine later to debug - could also try adding a space into the path that the CI runs in)
Wasn't a to reproduce in the test suite, but I have an idea - if true, it might be pip's fault.
I believe the issue is that `PIP_CONSTRAINTS="path with space.txt"` does not work - it's trying to define separate constraint files instead. This is the in the workaround from pypa/build#292.

Ideally, build would provide some interface to define constraints that would, for now, be converted to `--constraints` to pip but would be adapted to a new tool if pip was replaced - this workaround is only valid as long as pip is the package installer used.
Another work around is to use `file://` URLs (for which you can percent-escape the path).

Resolving patch:
diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py
--- a/cibuildwheel/macos.py
+++ b/cibuildwheel/macos.py
@@ -416,9 +416,8 @@ def build(options: BuildOptions) -> None:
                 config_setting = " ".join(verbosity_flags)
                 build_env = env.copy()
                 if options.dependency_constraints:
-                    build_env["PIP_CONSTRAINT"] = str(
-                        options.dependency_constraints.get_for_python_version(config.version)
-                    )
+                    constr = options.dependency_constraints.get_for_python_version(config.version)
+                    build_env["PIP_CONSTRAINT"] = constr.as_uri()
                 build_env["VIRTUALENV_PIP"] = get_pip_version(env)
                 call(
                     [
diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py
--- a/cibuildwheel/windows.py
+++ b/cibuildwheel/windows.py
@@ -23,9 +23,11 @@
     read_python_configs,
 )
 
+CIBW_INSTALL_PATH = Path("C:\\cibw")
+
 
 def call(
-    args: Sequence[PathOrStr], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None
+    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
@@ -33,7 +35,9 @@ def call(
     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[str] = None) -> None:
+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)
 
@@ -49,7 +53,7 @@ def get_nuget_args(version: str, arch: str) -> List[str]:
         "-FallbackSource",
         "https://api.nuget.org/v3/index.json",
         "-OutputDirectory",
-        "C:\\cibw\\python",
+        str(CIBW_INSTALL_PATH / "python"),
     ]
 
 
@@ -102,9 +106,9 @@ def install_pypy(version: str, arch: str, url: str) -> Path:
     zip_filename = url.rsplit("/", 1)[-1]
     extension = ".zip"
     assert zip_filename.endswith(extension)
-    installation_path = Path("C:\\cibw") / zip_filename[: -len(extension)]
+    installation_path = CIBW_INSTALL_PATH / zip_filename[: -len(extension)]
     if not installation_path.exists():
-        pypy_zip = Path("C:\\cibw") / zip_filename
+        pypy_zip = CIBW_INSTALL_PATH / zip_filename
         download(url, pypy_zip)
         # Extract to the parent directory because the zip file still contains a directory
         extract_zip(pypy_zip, installation_path.parent)
@@ -119,7 +123,7 @@ def setup_python(
     build_frontend: BuildFrontend,
 ) -> Dict[str, str]:
 
-    nuget = Path("C:\\cibw\\nuget.exe")
+    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)
@@ -184,7 +188,7 @@ def setup_python(
     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="C:\\cibw")
+        call(["python", "-m", "ensurepip"], env=env, cwd=CIBW_INSTALL_PATH)
 
     # upgrade pip to the version matching our constraints
     # if necessary, reinstall it to ensure that it's available on PATH as 'pip.exe'
@@ -199,7 +203,7 @@ def setup_python(
             *dependency_constraint_flags,
         ],
         env=env,
-        cwd="C:\\cibw",
+        cwd=CIBW_INSTALL_PATH,
     )
 
     assert (installation_path / "Scripts" / "pip.exe").exists()
@@ -313,22 +317,35 @@ def build(options: BuildOptions) -> None:
                 config_setting = " ".join(verbosity_flags)
                 build_env = env.copy()
                 if options.dependency_constraints:
-                    build_env["PIP_CONSTRAINT"] = str(
-                        options.dependency_constraints.get_for_python_version(config.version)
+                    constraints_path = options.dependency_constraints.get_for_python_version(
+                        config.version
+                    )
+                    # Bug in pip <= 21.1.3 - we can't have a space in the
+                    # constraints file, and pip doesn't support drive letters
+                    # in uhi.  After probably pip 21.2, we can use uri. For
+                    # now, use a temporary file.
+                    if " " in str(constraints_path):
+                        tmp_file = tempfile.NamedTemporaryFile(
+                            "w", suffix="constraints.txt", delete=False, dir=CIBW_INSTALL_PATH
+                        )
+                        with tmp_file as new_constraints_file, open(constraints_path) as f:
+                            new_constraints_file.write(f.read())
+                            constraints_path = Path(new_constraints_file.name)
+
+                    build_env["PIP_CONSTRAINT"] = str(constraints_path)
+                    build_env["VIRTUALENV_PIP"] = get_pip_version(env)
+                    call(
+                        [
+                            "python",
+                            "-m",
+                            "build",
+                            options.package_dir,
+                            "--wheel",
+                            f"--outdir={built_wheel_dir}",
+                            f"--config-setting={config_setting}",
+                        ],
+                        env=build_env,
                     )
-                build_env["VIRTUALENV_PIP"] = get_pip_version(env)
-                call(
-                    [
-                        "python",
-                        "-m",
-                        "build",
-                        options.package_dir,
-                        "--wheel",
-                        f"--outdir={built_wheel_dir}",
-                        f"--config-setting={config_setting}",
-                    ],
-                    env=build_env,
-                )
             else:
                 assert_never(options.build_frontend)
 


Test patch:
diff --git a/test/test_dependency_versions.py b/test/test_dependency_versions.py
--- a/test/test_dependency_versions.py
+++ b/test/test_dependency_versions.py
@@ -122,7 +122,7 @@ def test_dependency_constraints_file(tmp_path, build_frontend_env):
         "virtualenv": "20.0.35",
     }
 
-    constraints_file = tmp_path / "constraints.txt"
+    constraints_file = tmp_path / "constraints file.txt"
     constraints_file.write_text(
         textwrap.dedent(
             """