diff --git a/docs/changelog/1140.bugfix.rst b/docs/changelog/1140.bugfix.rst new file mode 100644 index 00000000..821a0d9f --- /dev/null +++ b/docs/changelog/1140.bugfix.rst @@ -0,0 +1,4 @@ +Pass ``--dependency-constraints-txt`` files through to the installer unmodified instead of re-parsing them into a +deduplicated set of lines, fixing a case where a hashed requirement (e.g. from ``pip-compile --generate-hashes``) could +have its ``--hash`` continuation line separated from its requirement line and silently dropped, depending on the +interpreter's hash seed - by :user:`manfred-kaiser` diff --git a/src/build/__main__.py b/src/build/__main__.py index fdcc9517..37e5afa0 100644 --- a/src/build/__main__.py +++ b/src/build/__main__.py @@ -224,7 +224,14 @@ def _bootstrap_build_env( install = env.install if dependency_constraints_txt: with open(dependency_constraints_txt, encoding='utf-8') as dependency_constraints_file: - install = partial(install, constraints=set(map(str.strip, dependency_constraints_file))) + constraints_text = dependency_constraints_file.read() + if constraints_text.strip(): + # Passed through as a single element instead of re-parsed into lines, so a requirement can never + # be split from its `--hash` continuation lines (as produced by `pip-compile --generate-hashes`) + # and silently lose its hash check. env.py's installer backends reconstruct the original file via + # `'\n'.join(constraints)` (see `_PipInstaller`/`_UvInstaller.install_dependencies`), a no-op here + # since there is only one element. + install = partial(install, constraints=(constraints_text,)) # first install the build dependencies install(builder.build_system_requires, _fresh=True) diff --git a/tests/test_main.py b/tests/test_main.py index 75aba559..348e196d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -457,7 +457,44 @@ def test_build_package_with_constraints( with pytest.raises(build.BuildBackendException, match=re.escape("Backend 'flit_core.buildapi' is not available.")): build.__main__.build_package(package_test_flit, tmp_path, ['wheel'], dependency_constraints_txt=constraints_txt_path) - install.assert_any_call({'flit_core >=2,<4'}, constraints={'flit-core==12.34', 'foo==wot'}, _fresh=True) + install.assert_any_call({'flit_core >=2,<4'}, constraints=('flit-core==12.34\nfoo==wot\n',), _fresh=True) + + +@pytest.mark.isolated +def test_build_package_with_constraints_passes_file_through_unmodified( + mocker: pytest_mock.MockerFixture, tmp_path: pathlib.Path, package_test_flit: str +) -> None: + # As produced by e.g. `pip-compile --generate-hashes`: a requirement and its --hash options wrapped onto + # continuation lines. Regression test for the requirement/hash pair being split apart when re-parsed into + # individual lines - the file content must reach the installer byte-for-byte instead. + install = mocker.patch('build.env.DefaultIsolatedEnv.install') + + constraints_text = ( + 'flit-core==12.34 \\\n --hash=sha256:aaaa \\\n --hash=sha256:bbbb\n # via test\nfoo==wot \\\n' + ' --hash=sha256:cccc\n' + ) + constraints_txt_path = tmp_path.joinpath('constraints.txt') + constraints_txt_path.write_text(constraints_text, encoding='utf-8') + + with pytest.raises(build.BuildBackendException, match=re.escape("Backend 'flit_core.buildapi' is not available.")): + build.__main__.build_package(package_test_flit, tmp_path, ['wheel'], dependency_constraints_txt=constraints_txt_path) + + install.assert_any_call({'flit_core >=2,<4'}, constraints=(constraints_text,), _fresh=True) + + +@pytest.mark.isolated +def test_build_package_with_empty_constraints_txt( + mocker: pytest_mock.MockerFixture, tmp_path: pathlib.Path, package_test_flit: str +) -> None: + install = mocker.patch('build.env.DefaultIsolatedEnv.install') + + constraints_txt_path = tmp_path.joinpath('constraints.txt') + constraints_txt_path.write_text(' \n\n', encoding='utf-8') + + with pytest.raises(build.BuildBackendException, match=re.escape("Backend 'flit_core.buildapi' is not available.")): + build.__main__.build_package(package_test_flit, tmp_path, ['wheel'], dependency_constraints_txt=constraints_txt_path) + + install.assert_any_call({'flit_core >=2,<4'}, _fresh=True) @pytest.mark.pypy3323bug