Skip to content

feat(bootstrap): add --multiple-versions flag to bootstrap all matching versions#1035

Merged
mergify[bot] merged 1 commit intopython-wheel-build:mainfrom
rd4398:bootstrap-multiple-version-flag
Apr 10, 2026
Merged

feat(bootstrap): add --multiple-versions flag to bootstrap all matching versions#1035
mergify[bot] merged 1 commit intopython-wheel-build:mainfrom
rd4398:bootstrap-multiple-version-flag

Conversation

@rd4398
Copy link
Copy Markdown
Contributor

@rd4398 rd4398 commented Apr 7, 2026

Add --multiple-versions flag to bootstrap command that enables bootstrapping all versions matching a requirement specification, rather than only the highest version. This is useful for creating comprehensive build environments with multiple versions of the same package.

Key changes:

  • Add return_all_versions parameter to BootstrapRequirementResolver.resolve() with type-safe overloads using @overload decorators
  • Modify Bootstrapper.bootstrap() to iterate over all resolved versions when --multiple-versions flag is enabled
  • Implement continue-on-error behavior: failed versions are logged, tracked, and reported at the end without stopping the bootstrap process
  • Add DependencyGraph.remove_dependency() to clean up failed nodes from graph
  • Apply recursively to entire dependency chain (not just top-level)

Testing:

  • Add 4 unit tests for resolver return_all_versions behavior
  • Add unit test for continue-on-error and graph cleanup behavior
  • Add e2e test using tomli>=2.0,<2.1 with constraints to verify multiple versions are bootstrapped successfully

Closes: #1036

@rd4398 rd4398 requested a review from a team as a code owner April 7, 2026 19:24
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces an opt-in multiple-versions bootstrap mode. CLI commands bootstrap and bootstrap_parallel gain --multiple-versions which is passed into Bootstrapper. BootstrapRequirementResolver.resolve adds return_all_versions and now returns a list of (url, Version) tuples (ordered highest-first). Bootstrapper accepts multiple_versions, resolves multiple candidates, and iterates bootstrapping per resolved version via a new _bootstrap_single_version, recording per-version failures in _failed_versions and removing failed nodes using a new DependencyGraph.remove_dependency. Tests and an e2e script exercise multi-version resolution and failure-tolerant behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly describes the main feature added: a --multiple-versions flag to the bootstrap command that enables bootstrapping all matching versions.
Description check ✅ Passed The description is well-detailed and directly relates to the changeset, explaining the feature, key implementation details, testing approach, and linking to the resolved issue.
Linked Issues check ✅ Passed All coding requirements from issue #1036 are met: flag added for opt-in behavior [#1036], resolver returns lists with return_all_versions parameter [#1036], bootstrapper iterates all versions when enabled [#1036], continue-on-error with tracking/reporting [#1036], DependencyGraph.remove_dependency() for cleanup [#1036], and unit/e2e tests provided [#1036].
Out of Scope Changes check ✅ Passed All changes align with issue #1036 requirements: CLI flag, resolver/bootstrapper updates for multi-version support, graph cleanup, error tracking, and comprehensive testing. No extraneous changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rd4398 rd4398 requested review from EmilienM and dhellmann April 7, 2026 19:25
@mergify mergify bot added the ci label Apr 7, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
tests/test_bootstrap_requirement_resolver_multiple.py (1)

16-33: Fixture shadows conftest tmp_context.

This file defines a local tmp_context fixture that returns a MagicMock instead of the real WorkContext from conftest. This is intentional for unit testing the resolver in isolation, but consider renaming to mock_context for clarity and to avoid confusion with the integration-level fixture.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_bootstrap_requirement_resolver_multiple.py` around lines 16 - 33,
The fixture tmp_context in this test shadows the integration-level conftest
fixture and returns a MagicMock WorkContext; rename this fixture (e.g.,
mock_context) to avoid confusion and clarify intent, update the fixture
definition name from tmp_context to mock_context and update all usages in this
test file (including any test function parameters) to use mock_context, keeping
the same MagicMock setup for WorkContext, ctx, ctx.package_build_info, and pbi
return values.
src/fromager/bootstrapper.py (1)

131-133: Consider clearing _failed_versions between top-level requirements.

The list accumulates failures across all top-level requirements. While the reporting at lines 313-324 filters by package name, consider if explicit clearing or a dict keyed by package would be cleaner for long bootstrap runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/bootstrapper.py` around lines 131 - 133, The _failed_versions
list in the bootstrapper (self._failed_versions: list[tuple[str, str,
Exception]]) currently accumulates failures across all top-level requirements;
change the logic so failures are cleared per top-level package run (or replace
the list with a dict keyed by package name to isolate failures) — specifically,
reset/clear self._failed_versions at the start of processing each top-level
requirement in the method that iterates top-level packages (or switch the
storage to something like self._failed_versions_by_package and record failures
under the current package key), and update any reporting code that reads
_failed_versions (e.g., the code that filters by package name at report time) to
use the new cleared list or per-package dict.
src/fromager/dependency_graph.py (1)

339-370: Parent edges on child nodes are not cleaned up.

When removing a node, the method clears incoming edges (from parents) but doesn't update the parents list on child nodes that referenced the removed node. If the removed node had children, those children will retain stale parent edge references.

In the current PR context (removing failed bootstrap versions), this is likely safe because failures occur before child dependencies are processed. However, for general API correctness:

Optional: Also clean up parent edges on children
         # Remove all edges pointing to this node from parent nodes
-        # Use list comprehension to filter, then clear and re-add
         for node in self.nodes.values():
             filtered_children = [
                 edge for edge in node.children if edge.destination_node.key != key
             ]
             node.children.clear()
             node.children.extend(filtered_children)
+
+            # Also clean up parent edges that reference the removed node
+            filtered_parents = [
+                edge for edge in node.parents if edge.destination_node.key != key
+            ]
+            node.parents.clear()
+            node.parents.extend(filtered_parents)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/dependency_graph.py` around lines 339 - 370, The
remove_dependency function deletes the node and removes parent->child edges from
remaining nodes but fails to update the removed node's children's parents lists,
leaving stale parent edges; fix by (before deleting self.nodes[key]) iterating
over the removed node's children (the list on the node being removed), and for
each child node filter/cleanup its parents list to remove any Edge whose source
(or source_node) key equals the removed key (i.e., remove parent edges
referencing the node being deleted), then proceed to delete the node and
continue filtering other nodes' children as currently implemented.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/test_bootstrap_multiple_versions.sh`:
- Around line 11-12: The trap currently expands $constraints_file when set
instead of at cleanup time; update the trap invocation that references the
constraints_file created by mktemp so it uses single quotes to defer expansion
(i.e., change the trap command that runs rm -f $constraints_file to use 'rm -f
$constraints_file') so the variable is evaluated when the trap runs rather than
when it is defined.

In `@src/fromager/bootstrapper.py`:
- Around line 292-308: TOP_LEVEL dependencies can be added twice when
multiple_versions is enabled because resolve_and_add_top_level() pre-adds the
highest version and bootstrap() re-adds all resolved versions; modify the
bootstrap() logic around the call to self.ctx.dependency_graph.add_dependency
(inside bootstrap() where req_type == RequirementType.TOP_LEVEL) to first check
for an existing parent->child edge (e.g., via an existing graph lookup method
like has_dependency / get_node / get_children or by checking DependencyNode
children for req.name + resolved_version) and only call add_dependency when that
exact dependency edge does not already exist; alternatively implement
deduplication inside DependencyNode.add_child() or add_dependency() so duplicate
parent edges are ignored.

In `@tests/test_bootstrapper.py`:
- Around line 298-302: Move the local import of canonicalize_name out of the
body of test_multiple_versions_continues_on_error and add a top-level import
statement "from packaging.utils import canonicalize_name" with the other imports
at the top of the file; then remove the inline "from packaging.utils import
canonicalize_name" line inside the test function so the test uses the top-level
canonicalize_name import.

---

Nitpick comments:
In `@src/fromager/bootstrapper.py`:
- Around line 131-133: The _failed_versions list in the bootstrapper
(self._failed_versions: list[tuple[str, str, Exception]]) currently accumulates
failures across all top-level requirements; change the logic so failures are
cleared per top-level package run (or replace the list with a dict keyed by
package name to isolate failures) — specifically, reset/clear
self._failed_versions at the start of processing each top-level requirement in
the method that iterates top-level packages (or switch the storage to something
like self._failed_versions_by_package and record failures under the current
package key), and update any reporting code that reads _failed_versions (e.g.,
the code that filters by package name at report time) to use the new cleared
list or per-package dict.

In `@src/fromager/dependency_graph.py`:
- Around line 339-370: The remove_dependency function deletes the node and
removes parent->child edges from remaining nodes but fails to update the removed
node's children's parents lists, leaving stale parent edges; fix by (before
deleting self.nodes[key]) iterating over the removed node's children (the list
on the node being removed), and for each child node filter/cleanup its parents
list to remove any Edge whose source (or source_node) key equals the removed key
(i.e., remove parent edges referencing the node being deleted), then proceed to
delete the node and continue filtering other nodes' children as currently
implemented.

In `@tests/test_bootstrap_requirement_resolver_multiple.py`:
- Around line 16-33: The fixture tmp_context in this test shadows the
integration-level conftest fixture and returns a MagicMock WorkContext; rename
this fixture (e.g., mock_context) to avoid confusion and clarify intent, update
the fixture definition name from tmp_context to mock_context and update all
usages in this test file (including any test function parameters) to use
mock_context, keeping the same MagicMock setup for WorkContext, ctx,
ctx.package_build_info, and pbi return values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b1fd183-63f7-431d-b992-1672b07d3d94

📥 Commits

Reviewing files that changed from the base of the PR and between d86f938 and 291e772.

📒 Files selected for processing (7)
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/dependency_graph.py
  • tests/test_bootstrap_requirement_resolver_multiple.py
  • tests/test_bootstrapper.py

@rd4398 rd4398 force-pushed the bootstrap-multiple-version-flag branch from b94cf5f to 60c563e Compare April 7, 2026 19:33
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
e2e/test_bootstrap_multiple_versions.sh (1)

11-12: ⚠️ Potential issue | 🟡 Minor

Use single quotes in trap to defer variable expansion.

Per SC2064: $constraints_file expands when the trap is set, not when triggered. If the variable changes or cleanup runs in a different context, this could fail silently.

 constraints_file=$(mktemp)
-trap "rm -f $constraints_file" EXIT
+trap 'rm -f "$constraints_file"' EXIT
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/test_bootstrap_multiple_versions.sh` around lines 11 - 12, The trap
currently expands $constraints_file at assignment time (SC2064); change the trap
invocation to use single quotes so the variable is expanded when the trap runs
(e.g., replace trap "rm -f $constraints_file" EXIT with a single-quoted form to
defer expansion), ensuring cleanup uses the final value of the constraints_file
variable.
src/fromager/bootstrapper.py (1)

292-308: ⚠️ Potential issue | 🟡 Minor

Duplicate graph edges for TOP_LEVEL when multiple_versions is enabled.

When multiple_versions=True, resolve_and_add_top_level() adds the highest version to the graph during pre-resolution. Then bootstrap() re-resolves all versions and adds each again at lines 298-307. The highest version gets added twice since add_dependency doesn't deduplicate.

Suggested fix: skip if already present
         for source_url, resolved_version in resolved_versions:
             # For top-level requirements, add to graph before bootstrapping
             # so that build dependencies can reference the parent node
             if req_type == RequirementType.TOP_LEVEL:
+                # Skip if already added (e.g., from pre-resolution phase)
+                node_key = f"{canonicalize_name(req.name)}=={resolved_version}"
+                if node_key in self.ctx.dependency_graph.nodes:
+                    logger.debug(f"skipping graph add for {node_key} - already present")
+                else:
-                pbi = self.ctx.package_build_info(req)
-                self.ctx.dependency_graph.add_dependency(
+                    pbi = self.ctx.package_build_info(req)
+                    self.ctx.dependency_graph.add_dependency(
                     parent_name=None,
                     parent_version=None,
                     req_type=RequirementType.TOP_LEVEL,
                     req=req,
                     req_version=resolved_version,
                     download_url=source_url,
                     pre_built=pbi.pre_built,
                     constraint=self.ctx.constraints.get_constraint(req.name),
                 )
-                self.ctx.write_to_graph_to_file()
+                    self.ctx.write_to_graph_to_file()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/bootstrapper.py` around lines 292 - 308, The TOP_LEVEL
dependency is being added again in bootstrap() for each resolved_version,
causing duplicates when multiple_versions=True; before calling
self.ctx.dependency_graph.add_dependency(...) in bootstrap() (the block that
handles RequirementType.TOP_LEVEL), check whether that exact dependency
(parent_name=None, parent_version=None, req=req, req_version=resolved_version)
already exists in the graph (e.g., via a has_dependency / contains / find API on
self.ctx.dependency_graph) and only call add_dependency and
self.ctx.write_to_graph_to_file() when it is not present, so the highest version
previously added by resolve_and_add_top_level() is skipped.
🧹 Nitpick comments (1)
src/fromager/bootstrapper.py (1)

131-133: Consider adding a docstring for _failed_versions structure.

The tuple structure (str, str, Exception) stores (canonicalized_name, version_str, exception). A brief inline comment clarifies intent but a type alias would improve maintainability.

         # Track failed versions in multiple_versions mode
-        # Maps (package_name, version) -> exception info
-        self._failed_versions: list[tuple[str, str, Exception]] = []
+        # List of (canonicalized_name, version_string, exception) for failed bootstraps
+        self._failed_versions: list[tuple[NormalizedName, str, Exception]] = []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/bootstrapper.py` around lines 131 - 133, Add a clear type alias
and docstring for the _failed_versions attribute to document the tuple shape and
intent: define a top-level type alias like FailedVersion = tuple[str, str,
Exception] (or a small NamedTuple/TypedDict for stronger typing) and replace the
inline annotation with list[FailedVersion], then add a one-line
docstring/comment above self._failed_versions explaining that each entry is
(canonicalized_name, version_str, exception) used to track failures in
multiple_versions mode; update references to _failed_versions if necessary to
use the new alias.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@e2e/test_bootstrap_multiple_versions.sh`:
- Around line 11-12: The trap currently expands $constraints_file at assignment
time (SC2064); change the trap invocation to use single quotes so the variable
is expanded when the trap runs (e.g., replace trap "rm -f $constraints_file"
EXIT with a single-quoted form to defer expansion), ensuring cleanup uses the
final value of the constraints_file variable.

In `@src/fromager/bootstrapper.py`:
- Around line 292-308: The TOP_LEVEL dependency is being added again in
bootstrap() for each resolved_version, causing duplicates when
multiple_versions=True; before calling
self.ctx.dependency_graph.add_dependency(...) in bootstrap() (the block that
handles RequirementType.TOP_LEVEL), check whether that exact dependency
(parent_name=None, parent_version=None, req=req, req_version=resolved_version)
already exists in the graph (e.g., via a has_dependency / contains / find API on
self.ctx.dependency_graph) and only call add_dependency and
self.ctx.write_to_graph_to_file() when it is not present, so the highest version
previously added by resolve_and_add_top_level() is skipped.

---

Nitpick comments:
In `@src/fromager/bootstrapper.py`:
- Around line 131-133: Add a clear type alias and docstring for the
_failed_versions attribute to document the tuple shape and intent: define a
top-level type alias like FailedVersion = tuple[str, str, Exception] (or a small
NamedTuple/TypedDict for stronger typing) and replace the inline annotation with
list[FailedVersion], then add a one-line docstring/comment above
self._failed_versions explaining that each entry is (canonicalized_name,
version_str, exception) used to track failures in multiple_versions mode; update
references to _failed_versions if necessary to use the new alias.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 00bcfaf8-ece6-4f00-9f01-ad051e400027

📥 Commits

Reviewing files that changed from the base of the PR and between 291e772 and 60c563e.

📒 Files selected for processing (8)
  • e2e/ci_bootstrap_suite.sh
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/dependency_graph.py
  • tests/test_bootstrap_requirement_resolver_multiple.py
  • tests/test_bootstrapper.py
✅ Files skipped from review due to trivial changes (3)
  • e2e/ci_bootstrap_suite.sh
  • tests/test_bootstrapper.py
  • tests/test_bootstrap_requirement_resolver_multiple.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/fromager/dependency_graph.py
  • src/fromager/bootstrap_requirement_resolver.py

@rd4398 rd4398 marked this pull request as draft April 7, 2026 20:31
@rd4398 rd4398 marked this pull request as ready for review April 7, 2026 20:31
@rd4398 rd4398 force-pushed the bootstrap-multiple-version-flag branch from 60c563e to 889df21 Compare April 8, 2026 13:46
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/fromager/commands/lint_requirements.py (1)

82-96: ⚠️ Potential issue | 🟠 Major

Stop after recording an invalid requirement.

The except path falls through into the resolver block. A bad line can either raise UnboundLocalError on the first failure or re-use the previous iteration’s requirement and resolve the wrong package.

Suggested fix
             except InvalidRequirement as err:
                 msg = f"{path}: {line}: {err}"
                 logger.error(msg)
                 failures.append(msg)
+                continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/commands/lint_requirements.py` around lines 82 - 96, The except
InvalidRequirement block logs and appends the failure but then continues into
the resolver code and may reuse an invalid or previous `requirement`; update the
control flow so that after handling the InvalidRequirement you skip resolution
(e.g., add an immediate continue/return from the loop or wrap the resolver in an
else) so the resolver block that uses `requirement`, `requirement_ctxvar.set`,
and calls `bt.resolve_versions(..., req_type=RequirementType.TOP_LEVEL)` only
runs when no InvalidRequirement was raised and `resolve_requirements` and `not
is_constraints` are true.
src/fromager/bootstrapper.py (1)

340-370: ⚠️ Potential issue | 🟠 Major

Undo graph/seen state when a version fails.

By this point the version has already been marked as seen and added to the graph. The multi-version failure path removes the node but leaves the seen entry behind, so a later encounter can re-add the node at Line 325 and then return early at Line 334 without retrying. When test_mode is also enabled, the early return at Lines 352-356 skips remove_dependency() entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/bootstrapper.py` around lines 340 - 370, The code marks a
version seen via _mark_as_seen and adds it to the dependency graph before
bootstrapping, but on exceptions the seen state is never undone (only the graph
node is removed in the multiple_versions branch), causing future early returns;
update the exception handling in the try/except inside _bootstrap_impl's caller
so that on any failure you undo the earlier mark and graph addition: either call
a new helper (e.g. _unmark_as_seen(req, resolved_version, build_sdist_only)) or
inline logic to remove the seen entry and remove_dependency from
ctx.dependency_graph, and invoke that cleanup in both the test_mode branch
(where _record_test_mode_failure is called) and the multiple_versions branch
(before returning), and ensure non-test/non-multi cases also perform cleanup
before re-raising or returning; use the existing symbols _mark_as_seen,
_record_test_mode_failure, multiple_versions, test_mode,
ctx.dependency_graph.remove_dependency to locate and implement the cleanup.
♻️ Duplicate comments (1)
e2e/test_bootstrap_multiple_versions.sh (1)

11-12: ⚠️ Potential issue | 🟡 Minor

Quote the trap command with single quotes.

$constraints_file is expanded when the trap is installed, not when it runs.

Suggested fix
 constraints_file=$(mktemp)
-trap "rm -f $constraints_file" EXIT
+trap 'rm -f "$constraints_file"' EXIT
As per coding guidelines, `e2e/**/*.sh`: Check for proper cleanup and teardown (trap handlers). Ensure variables are quoted to prevent word splitting.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/test_bootstrap_multiple_versions.sh` around lines 11 - 12, The trap
handler currently uses double quotes so $constraints_file is expanded at trap
installation instead of execution; change the trap invocation to use single
quotes so the variable is expanded when the trap runs and also ensure the rm
invocation inside the trap quotes the variable (constraints_file) to avoid
word-splitting when cleaning up.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fromager/bootstrapper.py`:
- Around line 276-291: resolve_versions can return an empty list which needs to
be treated as a resolution failure; after calling resolve_versions (the
resolved_versions variable) add a check for if not resolved_versions and then
mirror the existing exception handling: if not self.test_mode re-raise a new
error (or raise a ResolutionError) otherwise call
self._record_test_mode_failure(req, None, err_or_new_exception, "resolution")
and return so the subsequent for loop over resolved_versions does not silently
skip the dependency; use the same error-path conventions as the current except
block and reference resolve_versions, resolved_versions, self.test_mode,
self._record_test_mode_failure, and self._bootstrap_single_version to locate
where to insert this check.

---

Outside diff comments:
In `@src/fromager/bootstrapper.py`:
- Around line 340-370: The code marks a version seen via _mark_as_seen and adds
it to the dependency graph before bootstrapping, but on exceptions the seen
state is never undone (only the graph node is removed in the multiple_versions
branch), causing future early returns; update the exception handling in the
try/except inside _bootstrap_impl's caller so that on any failure you undo the
earlier mark and graph addition: either call a new helper (e.g.
_unmark_as_seen(req, resolved_version, build_sdist_only)) or inline logic to
remove the seen entry and remove_dependency from ctx.dependency_graph, and
invoke that cleanup in both the test_mode branch (where
_record_test_mode_failure is called) and the multiple_versions branch (before
returning), and ensure non-test/non-multi cases also perform cleanup before
re-raising or returning; use the existing symbols _mark_as_seen,
_record_test_mode_failure, multiple_versions, test_mode,
ctx.dependency_graph.remove_dependency to locate and implement the cleanup.

In `@src/fromager/commands/lint_requirements.py`:
- Around line 82-96: The except InvalidRequirement block logs and appends the
failure but then continues into the resolver code and may reuse an invalid or
previous `requirement`; update the control flow so that after handling the
InvalidRequirement you skip resolution (e.g., add an immediate continue/return
from the loop or wrap the resolver in an else) so the resolver block that uses
`requirement`, `requirement_ctxvar.set`, and calls `bt.resolve_versions(...,
req_type=RequirementType.TOP_LEVEL)` only runs when no InvalidRequirement was
raised and `resolve_requirements` and `not is_constraints` are true.

---

Duplicate comments:
In `@e2e/test_bootstrap_multiple_versions.sh`:
- Around line 11-12: The trap handler currently uses double quotes so
$constraints_file is expanded at trap installation instead of execution; change
the trap invocation to use single quotes so the variable is expanded when the
trap runs and also ensure the rm invocation inside the trap quotes the variable
(constraints_file) to avoid word-splitting when cleaning up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ccc3cfab-161e-4537-9baf-cfce8e73456a

📥 Commits

Reviewing files that changed from the base of the PR and between 60c563e and 889df21.

📒 Files selected for processing (11)
  • e2e/ci_bootstrap_suite.sh
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/commands/lint_requirements.py
  • src/fromager/dependency_graph.py
  • tests/test_bootstrap_requirement_resolver.py
  • tests/test_bootstrap_requirement_resolver_multiple.py
  • tests/test_bootstrap_test_mode.py
  • tests/test_bootstrapper.py
✅ Files skipped from review due to trivial changes (3)
  • e2e/ci_bootstrap_suite.sh
  • tests/test_bootstrapper.py
  • tests/test_bootstrap_requirement_resolver_multiple.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/fromager/dependency_graph.py
  • src/fromager/bootstrap_requirement_resolver.py

@LalatenduMohanty
Copy link
Copy Markdown
Member

The PR is related to #878 , as discussed with @rd4398 , he will create an issue for this PR specifically, as it will help with the context. The PR description is good enough but lets practice creating issues for features or bugs where we can continue design related discussion

@rd4398
Copy link
Copy Markdown
Contributor Author

rd4398 commented Apr 8, 2026

The PR is related to #878 , as discussed with @rd4398 , he will create an issue for this PR specifically, as it will help with the context. The PR description is good enough but lets practice creating issues for features or bugs where we can continue design related discussion

#1036 is the sub task in the feature for multiple version bootstrap. We are tracking these sub tasks downstream as well

Copy link
Copy Markdown
Member

@dhellmann dhellmann left a comment

Choose a reason for hiding this comment

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

This looks very close. There are some comments from CodeRabbit that look like they're worth investigating, so I'll leave it open for those updates. And I had one question about the handling of top-level dependencies that might lead to a change.

@rd4398 rd4398 force-pushed the bootstrap-multiple-version-flag branch from 2f0096e to 321a95f Compare April 8, 2026 19:03
@rd4398
Copy link
Copy Markdown
Contributor Author

rd4398 commented Apr 8, 2026

This looks very close. There are some comments from CodeRabbit that look like they're worth investigating, so I'll leave it open for those updates. And I had one question about the handling of top-level dependencies that might lead to a change.

Thank you! I have addressed the feedback from CodeRabbit as well

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_bootstrap_requirement_resolver_multiple.py (1)

167-172: Consider verifying highest-first ordering explicitly.

The comment says "should be sorted highest first" but the assertion only checks membership. For completeness, verify the ordering:

         # Should return both versions from graph
         assert len(results) == 2
         # Verify versions (should be sorted highest first)
-        versions = [v for _, v in results]
-        assert Version("2.0") in versions
-        assert Version("1.0") in versions
+        assert results[0][1] == Version("2.0")  # Highest first
+        assert results[1][1] == Version("1.0")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_bootstrap_requirement_resolver_multiple.py` around lines 167 -
172, The test currently only checks membership for results/versions but not that
they're sorted highest-first; update the assertions around results/versions in
tests/test_bootstrap_requirement_resolver_multiple.py to assert the ordering
explicitly (e.g., that the extracted versions list equals [Version("2.0"),
Version("1.0")] or that versions[0] == Version("2.0") and versions[1] ==
Version("1.0")). Use the existing variables results and versions and the
Version(...) constructor to perform the ordered check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fromager/bootstrap_requirement_resolver.py`:
- Around line 112-116: After calling self._resolve(...) and when using cached
results from self.cache_resolution(...), guard against an empty results list
before indexing results[0]: if not results then return [] when
return_all_versions is True, otherwise raise a clear exception (e.g.,
RequirementResolutionError or ValueError) that includes the request/specifier
context; apply this same empty-check to the cached-results branch and the
fresh-resolution branch so no IndexError can occur.

In `@src/fromager/dependency_graph.py`:
- Around line 363-370: In the node-removal loop (the code that filters
node.children by edge.destination_node.key), also remove the corresponding
parent references from the destination node so we don't leave dangling entries
in node.parents; specifically, when you detect an edge whose
destination_node.key == key, remove that edge from the current node.children and
also remove the matching parent edge/reference from
edge.destination_node.parents (or filter destination_node.parents to exclude
parents whose parent key == key). Update the same removal code in the method
that deletes nodes (referencing node.children, edge.destination_node, and
node.parents) so traversals like _collect_dependents() no longer see stale
parent references.

---

Nitpick comments:
In `@tests/test_bootstrap_requirement_resolver_multiple.py`:
- Around line 167-172: The test currently only checks membership for
results/versions but not that they're sorted highest-first; update the
assertions around results/versions in
tests/test_bootstrap_requirement_resolver_multiple.py to assert the ordering
explicitly (e.g., that the extracted versions list equals [Version("2.0"),
Version("1.0")] or that versions[0] == Version("2.0") and versions[1] ==
Version("1.0")). Use the existing variables results and versions and the
Version(...) constructor to perform the ordered check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8a114441-3a33-4eec-ac89-f9cf06ee30d4

📥 Commits

Reviewing files that changed from the base of the PR and between 889df21 and 321a95f.

📒 Files selected for processing (11)
  • e2e/ci_bootstrap_suite.sh
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/commands/lint_requirements.py
  • src/fromager/dependency_graph.py
  • tests/test_bootstrap_requirement_resolver.py
  • tests/test_bootstrap_requirement_resolver_multiple.py
  • tests/test_bootstrap_test_mode.py
  • tests/test_bootstrapper.py
✅ Files skipped from review due to trivial changes (1)
  • e2e/ci_bootstrap_suite.sh
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/test_bootstrap_test_mode.py
  • tests/test_bootstrap_requirement_resolver.py
  • tests/test_bootstrapper.py
  • src/fromager/commands/bootstrap.py

Copy link
Copy Markdown
Member

@LalatenduMohanty LalatenduMohanty left a comment

Choose a reason for hiding this comment

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

The review comments I have are not blocker for this PR.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/fromager/bootstrapper.py (1)

351-387: ⚠️ Potential issue | 🟠 Major

Clear the seen state when a version fails in multi-version mode.

Line 342 adds the node before the seen check, and Line 357 marks it seen before the build runs. If the build then fails, you remove the node from the graph but keep the seen marker. A later dependency on the same version will re-add the node, hit _has_been_seen(), and return early, leaving a failed version back in the graph.

Proposed fix
                 if self.multiple_versions:
                     pkg_name = canonicalize_name(req.name)
                     self._failed_versions.append((pkg_name, str(resolved_version), err))
+                    self._seen_requirements.discard(
+                        self._resolved_key(req, resolved_version, "sdist")
+                    )
+                    self._seen_requirements.discard(
+                        self._resolved_key(req, resolved_version, "wheel")
+                    )
                     logger.warning(
                         f"{req.name}=={resolved_version}: failed to bootstrap: {type(err).__name__}: {err}"
                     )
                     # Remove failed node from graph since bootstrap didn't complete
                     self.ctx.dependency_graph.remove_dependency(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/bootstrapper.py` around lines 351 - 387, When bootstrap fails in
multiple_versions mode we remove the node from the graph but forget to clear the
"seen" marker, so future attempts skip this version via _has_been_seen; modify
the exception handler in the block wrapping _bootstrap_impl to call whatever
clears the seen state (the inverse of _mark_as_seen) for the given req,
resolved_version, and build_sdist_only before appending to _failed_versions and
returning; ensure this cleanup runs only when self.multiple_versions is true
(and similarly for test_mode if applicable) so subsequent retries won't be
short-circuited by the stale seen marker.
♻️ Duplicate comments (3)
src/fromager/bootstrapper.py (1)

287-302: ⚠️ Potential issue | 🟠 Major

Keep the empty-resolution failure inside the test-mode error path.

resolve_versions(..., return_all_versions=True) can return []. Right now the RuntimeError is raised after the try/except, so --test-mode aborts instead of recording a resolution failure and continuing.

Proposed fix
         try:
             resolved_versions = self.resolve_versions(
                 req=req,
                 req_type=req_type,
                 return_all_versions=self.multiple_versions,
             )
+            if not resolved_versions:
+                raise RuntimeError(f"Could not resolve any versions for {req}")
             if self.multiple_versions:
                 logger.info(f"resolved {len(resolved_versions)} version(s) for {req}")
         except Exception as err:
             if not self.test_mode:
                 raise
             self._record_test_mode_failure(req, None, err, "resolution")
             return
-
-        # Check if resolution returned no versions
-        if not resolved_versions:
-            raise RuntimeError(f"Could not resolve any versions for {req}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/bootstrapper.py` around lines 287 - 302, The code raises
RuntimeError for empty resolved_versions outside the try/except which bypasses
test-mode handling; modify the empty-resolution check so it respects
self.test_mode: after calling resolve_versions (where resolved_versions may be
[]), if not resolved_versions then if not self.test_mode raise
RuntimeError(f"Could not resolve any versions for {req}") else call
self._record_test_mode_failure(req, None, RuntimeError(f"Could not resolve any
versions for {req}"), "resolution") and return; reference resolve_versions,
resolved_versions, self.test_mode, and _record_test_mode_failure to locate and
update the logic.
e2e/test_bootstrap_multiple_versions.sh (1)

11-12: ⚠️ Potential issue | 🟡 Minor

Use single quotes in the trap.

The current trap expands $constraints_file when the trap is set, not on exit. That can leave the temp file cleanup tied to the wrong value.

Proposed fix
 constraints_file=$(mktemp)
-trap "rm -f $constraints_file" EXIT
+trap 'rm -f "$constraints_file"' EXIT
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/test_bootstrap_multiple_versions.sh` around lines 11 - 12, The trap
currently interpolates $constraints_file at trap setup time, which can capture
the wrong filename; change the trap command so it defers expansion by using
single quotes around the command (i.e., update the trap line that references
constraints_file and EXIT) so the removal command runs with the actual value of
the constraints_file created by mktemp at script exit.
src/fromager/bootstrap_requirement_resolver.py (1)

107-116: ⚠️ Potential issue | 🟠 Major

Guard empty resolution results before indexing.

Both branches index [0]. If a specifier matches nothing, return_all_versions=True can legitimately produce [], and the default path then crashes with IndexError instead of raising a clear resolution error.

Proposed fix
         cached_result = self.get_cached_resolution(req, pre_built)
         if cached_result is not None:
             logger.debug(f"resolved {req} from cache")
-            return cached_result if return_all_versions else [cached_result[0]]
+            if return_all_versions:
+                return cached_result
+            if not cached_result:
+                raise ValueError(f"No matching versions found for {req}")
+            return [cached_result[0]]
@@
         results = self._resolve(req, req_type, parent_req, pre_built)
 
         # Cache the result
         self.cache_resolution(req, pre_built, results)
-        return results if return_all_versions else [results[0]]
+        if return_all_versions:
+            return results
+        if not results:
+            raise ValueError(f"No matching versions found for {req}")
+        return [results[0]]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/bootstrap_requirement_resolver.py` around lines 107 - 116, The
code currently indexes [0] on cached_result and results without checking for
emptiness causing IndexError when no versions match; in the resolve flow
(function/method using cached_result, self._resolve and self.cache_resolution)
guard both branches by checking if cached_result (when not None) or results is
empty and raise a clear resolution exception (e.g., a ResolutionError or
ValueError) before attempting to return [cached_result[0]] or [results[0]];
ensure return_all_versions still returns [] when requested and that the error is
raised only for the single-version return path when there are no matches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_bootstrap_requirement_resolver_multiple.py`:
- Around line 167-172: The test currently only checks membership of
Version("2.0") and Version("1.0") in versions extracted from results but claims
the list is "sorted highest first"; change the assertions to verify order
explicitly by asserting that versions equals the expected ordered list (e.g.
first Version("2.0"), then Version("1.0")) or by asserting versions[0] is
Version("2.0") and versions[1] is Version("1.0"); update the assertions that
reference results and Version to reflect this exact-order check so the test
fails on ordering regressions.

---

Outside diff comments:
In `@src/fromager/bootstrapper.py`:
- Around line 351-387: When bootstrap fails in multiple_versions mode we remove
the node from the graph but forget to clear the "seen" marker, so future
attempts skip this version via _has_been_seen; modify the exception handler in
the block wrapping _bootstrap_impl to call whatever clears the seen state (the
inverse of _mark_as_seen) for the given req, resolved_version, and
build_sdist_only before appending to _failed_versions and returning; ensure this
cleanup runs only when self.multiple_versions is true (and similarly for
test_mode if applicable) so subsequent retries won't be short-circuited by the
stale seen marker.

---

Duplicate comments:
In `@e2e/test_bootstrap_multiple_versions.sh`:
- Around line 11-12: The trap currently interpolates $constraints_file at trap
setup time, which can capture the wrong filename; change the trap command so it
defers expansion by using single quotes around the command (i.e., update the
trap line that references constraints_file and EXIT) so the removal command runs
with the actual value of the constraints_file created by mktemp at script exit.

In `@src/fromager/bootstrap_requirement_resolver.py`:
- Around line 107-116: The code currently indexes [0] on cached_result and
results without checking for emptiness causing IndexError when no versions
match; in the resolve flow (function/method using cached_result, self._resolve
and self.cache_resolution) guard both branches by checking if cached_result
(when not None) or results is empty and raise a clear resolution exception
(e.g., a ResolutionError or ValueError) before attempting to return
[cached_result[0]] or [results[0]]; ensure return_all_versions still returns []
when requested and that the error is raised only for the single-version return
path when there are no matches.

In `@src/fromager/bootstrapper.py`:
- Around line 287-302: The code raises RuntimeError for empty resolved_versions
outside the try/except which bypasses test-mode handling; modify the
empty-resolution check so it respects self.test_mode: after calling
resolve_versions (where resolved_versions may be []), if not resolved_versions
then if not self.test_mode raise RuntimeError(f"Could not resolve any versions
for {req}") else call self._record_test_mode_failure(req, None,
RuntimeError(f"Could not resolve any versions for {req}"), "resolution") and
return; reference resolve_versions, resolved_versions, self.test_mode, and
_record_test_mode_failure to locate and update the logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 599e8fc0-f085-4d49-a89d-61cde10ca115

📥 Commits

Reviewing files that changed from the base of the PR and between 321a95f and 72aad14.

📒 Files selected for processing (11)
  • e2e/ci_bootstrap_suite.sh
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/commands/lint_requirements.py
  • src/fromager/dependency_graph.py
  • tests/test_bootstrap_requirement_resolver.py
  • tests/test_bootstrap_requirement_resolver_multiple.py
  • tests/test_bootstrap_test_mode.py
  • tests/test_bootstrapper.py
✅ Files skipped from review due to trivial changes (3)
  • e2e/ci_bootstrap_suite.sh
  • tests/test_bootstrap_requirement_resolver.py
  • tests/test_bootstrapper.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/fromager/commands/lint_requirements.py
  • tests/test_bootstrap_test_mode.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/dependency_graph.py

@dhellmann
Copy link
Copy Markdown
Member

@rd4398 go ahead and resolve the conversation threads when have read them and are ready to merge this.

@rd4398
Copy link
Copy Markdown
Contributor Author

rd4398 commented Apr 10, 2026

@mergify merge

@mergify
Copy link
Copy Markdown
Contributor

mergify bot commented Apr 10, 2026

merge

❌ Sorry but I didn't understand the command. Please consult the commands documentation 📚.

@rd4398
Copy link
Copy Markdown
Contributor Author

rd4398 commented Apr 10, 2026

@mergify refresh

@mergify
Copy link
Copy Markdown
Contributor

mergify bot commented Apr 10, 2026

refresh

✅ Pull request refreshed

@LalatenduMohanty
Copy link
Copy Markdown
Member

@mergify rebase

…ng versions

Add --multiple-versions flag to bootstrap command that enables bootstrapping
all versions matching a requirement specification, rather than only the highest
version. This is useful for creating comprehensive build environments with
multiple versions of the same package.

Key changes:
- Add return_all_versions parameter to BootstrapRequirementResolver.resolve()
  with type-safe overloads using @overload decorators
- Modify Bootstrapper.bootstrap() to iterate over all resolved versions when
  --multiple-versions flag is enabled
- Implement continue-on-error behavior: failed versions are logged, tracked,
  and reported at the end without stopping the bootstrap process
- Add DependencyGraph.remove_dependency() to clean up failed nodes from graph
- Apply recursively to entire dependency chain (not just top-level)

Testing:
- Add 4 unit tests for resolver return_all_versions behavior
- Add unit test for continue-on-error and graph cleanup behavior
- Add e2e test using tomli>=2.0,<2.1 with constraints to verify multiple
  versions are bootstrapped successfully

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Signed-off-by: Rohan Devasthale <rdevasth@redhat.com>
@mergify
Copy link
Copy Markdown
Contributor

mergify bot commented Apr 10, 2026

Deprecation notice: This pull request comes from a fork and was rebased using bot_account impersonation. This capability will be removed on July 1, 2026. After this date, the rebase action will no longer be able to rebase fork pull requests with this configuration. Please switch to the update action/command to ensure compatibility going forward.

@mergify
Copy link
Copy Markdown
Contributor

mergify bot commented Apr 10, 2026

rebase

✅ Branch has been successfully rebased

@LalatenduMohanty LalatenduMohanty force-pushed the bootstrap-multiple-version-flag branch from 72aad14 to 30e2a28 Compare April 10, 2026 20:37
@mergify mergify bot merged commit b8f4441 into python-wheel-build:main Apr 10, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a flag to enable multiple version bootstrap

3 participants