Skip to content

Commit 11abef4

Browse files
feat(build): resolve build deps from dependency graph instead of PEP 517 hooks
build-sequence and build-parallel now use DependencyNode.iter_build_requirements() to populate build environments from the graph, eliminating the silent fallback to PEP 517 discovery hooks when cached dependency files were missing. build-sequence now requires a GRAPH_FILE argument alongside BUILD_ORDER_FILE. The fromager build command (debug/test) is unchanged. Closes: #996 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 2cf0e7d commit 11abef4

9 files changed

Lines changed: 221 additions & 24 deletions

File tree

docs/how-tos/bootstrap-constraints.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ production packages.
3131

3232
.. code-block:: console
3333
34-
$ fromager --constraints-file constraints.txt build-sequence ./work-dir/build-order.json
34+
$ fromager --constraints-file constraints.txt build-sequence ./work-dir/graph.json ./work-dir/build-order.json
3535
3636
This will use the constraints in the ``constraints.txt`` file to build the
3737
production packages for ``my-package``.

docs/using.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,10 @@ individual package compilation or integration into larger build systems.
187187

188188
### The build-sequence command
189189

190-
The `build-sequence` command processes a pre-determined build order file
191-
(typically `build-order.json`) to build wheels in dependency order.
190+
The `build-sequence` command processes a dependency graph (`graph.json`) and a
191+
pre-determined build order file (`build-order.json`) to build wheels in
192+
dependency order. Build dependencies are resolved from the graph rather than
193+
running PEP 517 discovery hooks.
192194

193195
The outputs are patched source distributions and built wheels for each item in
194196
the build-order file.
@@ -198,11 +200,12 @@ for any wheels that have already been built with the current settings.
198200

199201
For each package in the sequence:
200202

201-
1. **Build Order Reading** - Loads the build order file containing:
203+
1. **Build Order Reading** - Loads the build order and graph files:
202204

203-
- Package names and versions to build
204-
- Source URLs and types (PyPI, git, prebuilt)
205-
- Dependency relationships and constraints
205+
- `build-order.json`: Package names, versions, source URLs and types
206+
(PyPI, git, prebuilt) in predetermined build order
207+
- `graph.json`: Dependency relationships used to resolve build
208+
requirements without running PEP 517 discovery hooks
206209

207210
2. **Build Status Checking** - Determines if building is needed:
208211

e2e/test_build_order.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ fromager \
1919
--settings-dir="$SCRIPTDIR/changelog_settings" \
2020
bootstrap "${DIST}==${VERSION}"
2121

22-
# Save the build order file but remove everything else.
22+
# Save the build order and graph files but remove everything else.
2323
cp "$OUTDIR/work-dir/build-order.json" "$OUTDIR/"
24+
cp "$OUTDIR/work-dir/graph.json" "$OUTDIR/"
2425

2526
# Rebuild everything even if it already exists
2627
log="$OUTDIR/build-logs/${DIST}-build.log"
@@ -31,7 +32,7 @@ fromager \
3132
--sdists-repo "$OUTDIR/sdists-repo" \
3233
--wheels-repo "$OUTDIR/wheels-repo" \
3334
--settings-dir="$SCRIPTDIR/changelog_settings" \
34-
build-sequence --force "$OUTDIR/build-order.json"
35+
build-sequence --force "$OUTDIR/graph.json" "$OUTDIR/build-order.json"
3536

3637
find "$OUTDIR/wheels-repo/"
3738

@@ -94,7 +95,7 @@ fromager \
9495
--sdists-repo "$OUTDIR/sdists-repo" \
9596
--wheels-repo "$OUTDIR/wheels-repo" \
9697
--settings-dir="$SCRIPTDIR/changelog_settings" \
97-
build-sequence "$OUTDIR/build-order.json"
98+
build-sequence "$OUTDIR/graph.json" "$OUTDIR/build-order.json"
9899

99100
find "$OUTDIR/wheels-repo/"
100101

@@ -118,7 +119,7 @@ fromager \
118119
--work-dir "$OUTDIR/work-dir" \
119120
--sdists-repo "$OUTDIR/sdists-repo" \
120121
--wheels-repo "$OUTDIR/wheels-repo" \
121-
build-sequence --cache-wheel-server-url="https://pypi.org/simple" "$OUTDIR/build-order.json"
122+
build-sequence --cache-wheel-server-url="https://pypi.org/simple" "$OUTDIR/graph.json" "$OUTDIR/build-order.json"
122123

123124
find "$OUTDIR/wheels-repo/"
124125

e2e/test_build_sequence_git_url.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ls "$OUTDIR"/work-dir/*/build.log || true
2323

2424
# Clean up the work directory so we can test build-sequence
2525
mv "$OUTDIR/work-dir/build-order.json" "$OUTDIR/"
26+
cp "$OUTDIR/work-dir/graph.json" "$OUTDIR/"
2627
rm -rf "$OUTDIR/work-dir/wheels-repo"
2728
rm -rf "$OUTDIR/work-dir/sdists-repo"
2829

@@ -37,7 +38,7 @@ fromager \
3738
--sdists-repo "$OUTDIR/sdists-repo" \
3839
--wheels-repo "$OUTDIR/wheels-repo" \
3940
--settings-dir="$SCRIPTDIR/changelog_settings" \
40-
build-sequence --force "$OUTDIR/build-order.json"
41+
build-sequence --force "$OUTDIR/graph.json" "$OUTDIR/build-order.json"
4142

4243
find "$OUTDIR/wheels-repo/" -name '*.whl'
4344
find "$OUTDIR/sdists-repo/" -name '*.tar.gz'

e2e/test_prebuilt_wheel_hook.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ fromager \
2121
--settings-dir="$SCRIPTDIR/prebuilt_settings" \
2222
bootstrap "${DIST}==${VERSION}"
2323

24-
# Save the build order file but remove everything else.
24+
# Save the build order and graph files but remove everything else.
2525
cp "$OUTDIR/work-dir/build-order.json" "$OUTDIR/"
26+
cp "$OUTDIR/work-dir/graph.json" "$OUTDIR/"
2627

2728
# Remove downloaded wheels to trigger hook
2829
rm -rf "$OUTDIR/wheels-repo"
@@ -34,7 +35,7 @@ fromager \
3435
--sdists-repo "$OUTDIR/sdists-repo" \
3536
--wheels-repo "$OUTDIR/wheels-repo" \
3637
--settings-dir="$SCRIPTDIR/prebuilt_settings" \
37-
build-sequence "$OUTDIR/build-order.json"
38+
build-sequence "$OUTDIR/graph.json" "$OUTDIR/build-order.json"
3839

3940
PATTERNS=(
4041
"downloading prebuilt wheel ${DIST}==${VERSION}"

src/fromager/build_environment.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from .requirements_file import RequirementType
1919

2020
if typing.TYPE_CHECKING:
21-
from . import context
21+
from . import context, dependency_graph
2222

2323
logger = logging.getLogger(__name__)
2424

@@ -314,6 +314,55 @@ def prepare_build_environment(
314314
return build_env
315315

316316

317+
@metrics.timeit(description="prepare build environment from graph")
318+
def prepare_build_environment_from_graph(
319+
*,
320+
ctx: context.WorkContext,
321+
req: Requirement,
322+
sdist_root_dir: pathlib.Path,
323+
build_requirements: typing.Iterable[dependency_graph.DependencyNode],
324+
) -> BuildEnvironment:
325+
"""Create a build environment populated from pre-resolved graph dependencies.
326+
327+
Uses build requirements extracted from the dependency graph instead of
328+
running PEP 517 discovery hooks. This is the preferred path for Stage 2
329+
build commands (build-sequence, build-parallel) where the graph is the
330+
source of truth.
331+
"""
332+
logger.info("preparing build environment from dependency graph")
333+
334+
build_env = BuildEnvironment(
335+
ctx=ctx,
336+
parent_dir=sdist_root_dir.parent,
337+
)
338+
339+
reqs = {
340+
Requirement(f"{node.canonicalized_name}=={node.version}")
341+
for node in build_requirements
342+
}
343+
if reqs:
344+
# Graph-resolved deps are a mix of build-system, build-backend,
345+
# build-sdist, and their transitive install deps. We use
346+
# BUILD_SYSTEM as the label since they all go into the build env.
347+
_safe_install(
348+
ctx=ctx,
349+
req=req,
350+
build_env=build_env,
351+
deps=reqs,
352+
dep_req_type=RequirementType.BUILD_SYSTEM,
353+
)
354+
355+
try:
356+
distributions = build_env.get_distributions()
357+
except Exception:
358+
# ignore error for debug call, error reason is logged in get_distributions()
359+
pass
360+
else:
361+
logger.debug("build env %r has packages %r", build_env.path, distributions)
362+
363+
return build_env
364+
365+
317366
def _safe_install(
318367
ctx: context.WorkContext,
319368
req: Requirement,

src/fromager/commands/bootstrap.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ def bootstrap(
186186
progressbar.update()
187187
requirement_ctxvar.reset(token)
188188

189+
# Ensure graph.json is written even when no recursive dependencies
190+
# were discovered (e.g., prebuilt-only bootstraps).
191+
wkctx.write_to_graph_to_file()
192+
189193
# Finalize test mode and check for failures
190194
exit_code = bt.finalize()
191195
if exit_code != 0:

src/fromager/commands/build.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,22 +142,26 @@ def build(
142142
"cache_wheel_server_url",
143143
help="url to a wheel server from where fromager can check if it had already built the wheel",
144144
)
145+
@click.argument("graph_file")
145146
@click.argument("build_order_file")
146147
@click.pass_obj
147148
def build_sequence(
148149
wkctx: context.WorkContext,
150+
graph_file: str,
149151
build_order_file: str,
150152
force: bool,
151153
cache_wheel_server_url: str | None,
152154
) -> None:
153155
"""Build a sequence of wheels in order
154156
155-
BUILD_ORDER_FILE is the build-order.json files to build
157+
GRAPH_FILE is a graph.json file containing the dependency relationships
158+
between packages, used to resolve build dependencies.
156159
157-
SDIST_SERVER_URL is the URL for a PyPI-compatible package index hosting sdists
160+
BUILD_ORDER_FILE is the build-order.json file specifying the build order.
158161
159162
Performs the equivalent of the 'build' command for each item in
160-
the build order file.
163+
the build order file, using the dependency graph to populate
164+
build environments instead of PEP 517 discovery hooks.
161165
162166
"""
163167
server.start_wheel_server(wkctx)
@@ -173,6 +177,9 @@ def build_sequence(
173177
f"{wkctx.wheel_server_url=}, {cache_wheel_server_url=}"
174178
)
175179

180+
logger.info("reading dependency graph from %s", graph_file)
181+
graph = dependency_graph.DependencyGraph.from_file(graph_file)
182+
176183
entries: list[BuildSequenceEntry] = []
177184

178185
logger.info("reading build order from %s", build_order_file)
@@ -191,6 +198,10 @@ def build_sequence(
191198
else:
192199
req = Requirement(f"{dist_name}=={resolved_version}")
193200

201+
build_requirements = _get_build_requirements_from_graph(
202+
graph, dist_name, resolved_version
203+
)
204+
194205
with req_ctxvar_context(req, resolved_version):
195206
logger.info("building %s", resolved_version)
196207
entry = _build(
@@ -200,6 +211,7 @@ def build_sequence(
200211
source_download_url=source_download_url,
201212
force=force,
202213
cache_wheel_server_url=cache_wheel_server_url,
214+
build_requirements=build_requirements,
203215
)
204216
if entry.prebuilt:
205217
logger.info(
@@ -326,6 +338,7 @@ def _build(
326338
source_download_url: str,
327339
force: bool,
328340
cache_wheel_server_url: str | None,
341+
build_requirements: typing.Iterable[dependency_graph.DependencyNode] | None = None,
329342
) -> BuildSequenceEntry:
330343
"""Handle one version of one wheel.
331344
@@ -417,12 +430,20 @@ def _build(
417430
)
418431

419432
# Build environment
420-
build_env = build_environment.prepare_build_environment(
421-
ctx=wkctx,
422-
req=req,
423-
version=resolved_version,
424-
sdist_root_dir=source_root_dir,
425-
)
433+
if build_requirements is not None:
434+
build_env = build_environment.prepare_build_environment_from_graph(
435+
ctx=wkctx,
436+
req=req,
437+
sdist_root_dir=source_root_dir,
438+
build_requirements=build_requirements,
439+
)
440+
else:
441+
build_env = build_environment.prepare_build_environment(
442+
ctx=wkctx,
443+
req=req,
444+
version=resolved_version,
445+
sdist_root_dir=source_root_dir,
446+
)
426447

427448
# Make a new source distribution, in case we patched the code.
428449
sdist_filename = sources.build_sdist(
@@ -544,6 +565,7 @@ def _build_parallel(
544565
source_download_url: str,
545566
force: bool,
546567
cache_wheel_server_url: str | None,
568+
build_requirements: typing.Iterable[dependency_graph.DependencyNode] | None = None,
547569
) -> BuildSequenceEntry:
548570
"""
549571
This function runs in a thread to manage the build of a single package.
@@ -556,7 +578,24 @@ def _build_parallel(
556578
source_download_url=source_download_url,
557579
force=force,
558580
cache_wheel_server_url=cache_wheel_server_url,
581+
build_requirements=build_requirements,
582+
)
583+
584+
585+
def _get_build_requirements_from_graph(
586+
graph: dependency_graph.DependencyGraph,
587+
dist_name: str,
588+
version: Version,
589+
) -> list[dependency_graph.DependencyNode]:
590+
"""Look up build requirements for a package from the dependency graph."""
591+
node_key = f"{canonicalize_name(dist_name)}=={version}"
592+
node = graph.nodes.get(node_key)
593+
if node is None:
594+
raise KeyError(
595+
f"package {node_key} not found in dependency graph; "
596+
f"ensure the graph file matches the build order"
559597
)
598+
return list(node.iter_build_requirements())
560599

561600

562601
def _nodes_to_string(nodes: typing.Iterable[dependency_graph.DependencyNode]) -> str:
@@ -681,6 +720,7 @@ def update_progressbar_cb(future: concurrent.futures.Future) -> None:
681720
source_download_url=node.download_url,
682721
force=force,
683722
cache_wheel_server_url=cache_wheel_server_url,
723+
build_requirements=list(node.iter_build_requirements()),
684724
)
685725
future.add_done_callback(update_progressbar_cb)
686726
future2node[future] = node

0 commit comments

Comments
 (0)