Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions pyodide_build/build_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,16 @@ def _create_constraints_file() -> str:
if not constraints:
return ""

if len(constraints.split(maxsplit=1)) > 1:
raise ValueError(
"PIP_CONSTRAINT contains spaces so pip will misinterpret it. Make sure the path to pyodide has no spaces.\n"
"See https://github.com/pypa/pip/issues/13283"
)

# If a path to a file specified PIP_CONSTRAINT contains spaces, pip will misinterpret
# it as multiple files; see https://github.com/pypa/pip/issues/13283
# We work around this by converting the path to a URI.
constraints_file = Path(constraints)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose you'd have to split and take the first entry if you're keeping the following code? constraints might contain two files, so Path(constraints) wouldn't refer to a file.

constraints = (
constraints_file.as_uri()
if " " in str(constraints_file)
else str(constraints_file)
)

if not constraints_file.is_file():
constraints_file.parent.mkdir(parents=True, exist_ok=True)
constraints_file.write_text("")
Expand Down
26 changes: 18 additions & 8 deletions pyodide_build/recipe/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,9 @@ def __init__(
self.build_dir = (
Path(build_dir).resolve() if build_dir else self.pkg_root / "build"
)
if len(str(self.build_dir).split(maxsplit=1)) > 1:
raise ValueError(
"PIP_CONSTRAINT contains spaces so pip will misinterpret it. Make sure the path to the package build directory has no spaces.\n"
"See https://github.com/pypa/pip/issues/13283"
)
# If a path to a file specified PIP_CONSTRAINT contains spaces, pip will misinterpret
# it as multiple files; see https://github.com/pypa/pip/issues/13283
# We work around this by converting the path to a URI when needed.
self.library_install_prefix = self.build_dir.parent.parent / ".libs"
self.src_extract_dir = (
self.build_dir / self.fullname
Expand Down Expand Up @@ -367,7 +365,7 @@ def _download_and_extract(self) -> None:
shutil.move(self.build_dir / extract_dir_name, self.src_extract_dir)
self.src_dist_dir.mkdir(parents=True, exist_ok=True)

def _create_constraints_file(self) -> str:
def _create_constraints_file(self, filename: str = "constraints.txt") -> str:
"""
Creates a pip constraints file by concatenating global constraints (PIP_CONSTRAINT)
with constraints specific to this package.
Expand All @@ -381,12 +379,24 @@ def _create_constraints_file(self) -> str:
# nothing to override
return host_constraints

new_constraints_file = self.build_dir / "constraints.txt"
new_constraints_file = self.build_dir / filename
with new_constraints_file.open("w") as f:
for constraint in constraints:
f.write(constraint + "\n")

return host_constraints + " " + str(new_constraints_file)
# If a path to a file specified PIP_CONSTRAINT contains spaces, pip will misinterpret
# it as multiple files; see https://github.com/pypa/pip/issues/13283
# We work around this by converting the path to a URI.
new_constraints_str = (
new_constraints_file.as_uri()
if " " in str(new_constraints_file)
else str(new_constraints_file)
)

if host_constraints:
return host_constraints + " " + new_constraints_str
else:
return new_constraints_str

def _compile(
self,
Expand Down
23 changes: 23 additions & 0 deletions pyodide_build/tests/recipe/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,29 @@ def test_create_constraints_file_override(tmp_path, dummy_xbuildenv):
assert data[-3:] == ["numpy < 2.0", "pytest == 7.0", "setuptools < 75"], data


def test_create_constraints_file_space_in_path_uri_conversion(
tmp_path, dummy_xbuildenv
):
build_dir_with_spaces = tmp_path / "build dir with spaces"
build_dir_with_spaces.mkdir()

builder = RecipeBuilder.get_builder(
recipe=RECIPE_DIR / "pkg_test_constraint",
build_args=BuildArgs(),
build_dir=build_dir_with_spaces,
)

paths = builder._create_constraints_file(filename="constraints with space.txt")

parts = paths.split()
if len(parts) > 1:
last_part = parts[-1]
if "with%20space" in last_part or last_part.startswith("file://"):
assert True
else:
assert "constraints with space.txt" in last_part


class MockSourceSpec(_SourceSpec):
@pydantic.model_validator(mode="after")
def _check_patches_extra(self) -> Self:
Expand Down
20 changes: 20 additions & 0 deletions pyodide_build/tests/test_build_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,23 @@ def test_wheel_paths(dummy_xbuildenv):
f"{current_version}-none-any",
]
)


def test_create_constraints_file_with_spaces(tmp_path, monkeypatch, reset_cache):
from pyodide_build.build_env import _create_constraints_file

constraints_dir = tmp_path / "path with spaces"
constraints_dir.mkdir()
constraints_file = constraints_dir / "constraints.txt"
constraints_file.write_text("numpy==1.0\n")

def mock_get_build_flag(name):
if name == "PIP_CONSTRAINT":
return str(constraints_file)

monkeypatch.setattr("pyodide_build.build_env.get_build_flag", mock_get_build_flag)

result = _create_constraints_file()

assert result.startswith("file://")
assert "path%20with%20spaces" in result or "path with spaces" in result