From 3d7b57b12930973ee947087e48a3f40393c3c4a2 Mon Sep 17 00:00:00 2001 From: Vikash Shaw Date: Wed, 12 Aug 2026 20:14:22 -0400 Subject: [PATCH] feat: add settings-driven build_system_dependencies_hook Add a configurable hook in the global settings file that post-processes build-system dependencies after the per-package override (or default) runs and before marker filtering. The hook is specified as a dotted import path and loaded via Pydantic ImportString, following the same pattern as the proposed build_tag_hook. This enables downstream projects to implement cross-cutting concerns (such as setuptools version capping) as a single callable instead of duplicating per-package plugins. Closes #1263 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Vikash Shaw --- src/fromager/dependencies.py | 9 ++ src/fromager/packagesettings/_settings.py | 25 ++++++ tests/test_dependencies.py | 101 ++++++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/src/fromager/dependencies.py b/src/fromager/dependencies.py index d80899e8..f191cd28 100644 --- a/src/fromager/dependencies.py +++ b/src/fromager/dependencies.py @@ -65,6 +65,15 @@ def get_build_system_dependencies( sdist_root_dir=sdist_root_dir, build_dir=pbi.build_dir(sdist_root_dir), ) + hook = ctx.settings.build_system_dependencies_hook + if hook is not None: + orig_deps = hook( + ctx=ctx, + req=req, + sdist_root_dir=sdist_root_dir, + build_dir=pbi.build_dir(sdist_root_dir), + requirements=list(orig_deps), + ) deps = _filter_requirements(req, orig_deps) _write_requirements_file( diff --git a/src/fromager/packagesettings/_settings.py b/src/fromager/packagesettings/_settings.py index 888e6b22..87a9f7f9 100644 --- a/src/fromager/packagesettings/_settings.py +++ b/src/fromager/packagesettings/_settings.py @@ -55,6 +55,26 @@ class SettingsFile(pydantic.BaseModel): .. versionadded:: 0.92.0 """ + build_system_dependencies_hook: pydantic.ImportString | None = None + """Post-processing hook for build-system dependencies + + A dotted import path to a callable that receives build-system + dependencies after the per-package override (or default) runs + and returns a (possibly modified) list. When not set, no + post-processing is applied. + + The callable signature must be:: + + def hook( + *, + ctx: WorkContext, + req: Requirement, + sdist_root_dir: pathlib.Path, + build_dir: pathlib.Path, + requirements: list[str], + ) -> list[str]: ... + """ + @classmethod def from_string( cls, @@ -193,6 +213,11 @@ def external_commands(self) -> ExternalCommands: """ return self._settings.external_commands + @property + def build_system_dependencies_hook(self) -> typing.Callable[..., list[str]] | None: + """Get the build-system dependencies post-processing hook, if configured.""" + return self._settings.build_system_dependencies_hook + def variant_changelog(self) -> list[str]: """Get global changelog for current variant""" return list(self._settings.changelog.get(self.variant, [])) diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index 6f83a93e..bd03f1fd 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -515,3 +515,104 @@ def test_get_metadata_from_wheel_validation_disabled(tmp_path: pathlib.Path) -> # Assert: Should still parse the basic fields assert metadata.name == "testpkg" assert str(metadata.version) == "1.0.0" + + +def _sample_build_system_deps_hook( + *, + ctx: context.WorkContext, + req: Requirement, + sdist_root_dir: pathlib.Path, + build_dir: pathlib.Path, + requirements: list[str], +) -> list[str]: + return requirements + ["extra-dep>=1.0"] + + +@patch("fromager.dependencies._write_requirements_file") +@_clean_build_artifacts +def test_get_build_system_dependencies_with_hook( + _: Mock, tmp_path: pathlib.Path +) -> None: + from fromager import packagesettings + + settings = packagesettings.Settings( + settings=packagesettings.SettingsFile( + build_system_dependencies_hook=_sample_build_system_deps_hook, + ), + package_settings=[], + variant="cpu", + patches_dir=tmp_path / "patches", + max_jobs=None, + ) + ctx = context.WorkContext( + active_settings=settings, + patches_dir=tmp_path / "patches", + sdists_repo=tmp_path / "sdists-repo", + wheels_repo=tmp_path / "wheels-repo", + work_dir=tmp_path / "work-dir", + variant="cpu", + ) + ctx.setup() + + pyproject_file = _fromager_root / "pyproject.toml" + shutil.copyfile(pyproject_file, tmp_path / "pyproject.toml") + + results = dependencies.get_build_system_dependencies( + ctx=ctx, + req=Requirement("fromager"), + version=Version("1.0.0"), + sdist_root_dir=tmp_path, + ) + names = set(r.name for r in results) + assert "extra-dep" in names + assert "hatchling" in names + + +@patch("fromager.dependencies._write_requirements_file") +@_clean_build_artifacts +def test_get_build_system_dependencies_without_hook( + _: Mock, tmp_context: context.WorkContext, tmp_path: pathlib.Path +) -> None: + pyproject_file = _fromager_root / "pyproject.toml" + shutil.copyfile(pyproject_file, tmp_path / "pyproject.toml") + + results = dependencies.get_build_system_dependencies( + ctx=tmp_context, + req=Requirement("fromager"), + version=Version("1.0.0"), + sdist_root_dir=tmp_path, + ) + names = set(r.name for r in results) + assert "extra-dep" not in names + assert "hatchling" in names + + +def test_settings_file_build_system_dependencies_hook_default() -> None: + from fromager import packagesettings + + sf = packagesettings.SettingsFile() + assert sf.build_system_dependencies_hook is None + + +def test_settings_file_build_system_dependencies_hook_callable() -> None: + from fromager import packagesettings + + sf = packagesettings.SettingsFile( + build_system_dependencies_hook=_sample_build_system_deps_hook, + ) + assert sf.build_system_dependencies_hook is _sample_build_system_deps_hook + + +def test_settings_build_system_dependencies_hook_property() -> None: + from fromager import packagesettings + + settings = packagesettings.Settings( + settings=packagesettings.SettingsFile( + build_system_dependencies_hook=_sample_build_system_deps_hook, + ), + package_settings=[], + variant="cpu", + patches_dir=pathlib.Path("/tmp/patches"), + max_jobs=None, + ) + assert settings.build_system_dependencies_hook is _sample_build_system_deps_hook