Skip to content

fix(build): register run-export-introduced source deps on the assembled record - #6519

Merged
Hofer-Julian merged 3 commits into
prefix-dev:mainfrom
wolfv:fix/recursive-source-run-exports
Jul 6, 2026
Merged

fix(build): register run-export-introduced source deps on the assembled record#6519
Hofer-Julian merged 3 commits into
prefix-dev:mainfrom
wolfv:fix/recursive-source-run-exports

Conversation

@wolfv

@wolfvwolfv commented Jul 3, 2026

Copy link
Copy Markdown
Member

Fixes#6482.

Problem

When a source package's build/host environment contains another source-built package that declares run-exports (e.g. python_abi exporting python_abi 3.16.* *_asan_cp316), the exported spec is injected into the consumer's run dependencies — but extract_run_exports parsed run-export strings into BinarySpec unconditionally. The assembled record's sources map therefore never learned about the dependency, the parent environment's source walk (walk_and_resolve) never recursed into it (resolved_records=1 in the issue's logs), and the solve failed with:

└─ numpy 2.6.0.dev0 would require
└─ python_abi 3.16.* *_asan_cp316, for which no candidates were found.

The host env of numpy solved fine only because python_abi was an explicit host dependency there.

Changes

  • extract_run_exports now maps each exported package name against the solved environment's records; if the name resolved to a source record, the run-export stays a source spec carrying that record's pinned location (matchspec selectors are preserved, so the stringified depends in the lock file are unchanged). This also covers the same hole one layer down, where build-env strong exports feed the nested host solve.
  • resolve_source_record registers run-export-introduced source deps into the record's sources map leniently — an explicit source spec for the same package wins — since the pinned location can spell the same source differently than a manifest spec (pinned commit vs. branch). Pinned path locations are workspace-root-relative while sources entries are read relative to the record's manifest, so they are relativized first via a new SourceAnchor::relativize_location (the inverse of resolve_location; git/URL locations anchor as identity and pass through).
  • Fix an inverted condition in the DuplicateSourceDependency check: it errored when the same package was registered twice from the same location and silently kept the first when the locations genuinely differed — the opposite of the error message's intent.
  • Satisfiability mirror: collect_direct_run_exports only read run-exports off binary records while the solve path reads them off source records too; a locked mutable source with a source host dep that has run-exports would look perpetually unsatisfied. It now reads the package record for both shapes.

Tests

  • New integration test test_run_export_on_source_host_dependency + tests/data/workspaces/run-exports-source workspace modelling the issue: package_a host-depends on source package_b, which run-exports itself. Without the fix it fails with exactly the issue's error shape; with the fix package_b is part of the solution as a source record and registered in package_a's sources. The passthrough backend learned to serve an output's own run-exports from the instantiator's run_exports map (keyed by the package's own name).
  • Roundtrip unit tests for SourceAnchor::relativize_location.

Not addressed here: the issue also shows the error nondeterministically naming *_cp316 vs *_asan_cp316, which points at unstable variant selection in the host solve when several variants satisfy a bare spec — worth a separate look.

🤖 Generated with Claude Code

…ed record
A run dependency injected purely via the run-exports of a source-built
build/host dependency (e.g. python_abi exporting itself) was converted
to a plain binary matchspec, so the consuming record's `sources` map
never learned about it. The parent environment's source walk then never
recursed into the package and the solve failed with "no candidates
found" for a package that only exists as a source build.
- extract_run_exports now maps exported names back to source records in
the solved env and keeps them source-typed, carrying the pinned
location.
- resolve_source_record registers those locations (relativized to the
record's manifest so consumer-side anchoring roundtrips) leniently:
an explicit source spec for the same package wins.
- Fix the inverted DuplicateSourceDependency condition: it errored on
two identical locations and silently ignored genuinely different ones.
- The satisfiability mirror now reads run-exports off source records
too, matching the solve path.
- SourceAnchor::relativize_location: inverse of resolve_location for
workspace-root-relative pinned locations.
Fixesprefix-dev#6482
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Hofer-Julian
Hofer-Julian self-requested a review July 3, 2026 11:39
@lucascolley

Copy link
Copy Markdown
Collaborator

not working yet:

~/sandbox/v3-panic via 🧚 v0.72.0bat pixi.toml -p[workspace]channels = ["https://prefix.dev/conda-forge"]platforms = ["osx-arm64", "linux-64"]preview = ["pixi-build"][dependencies]
# numpy.git = "https://github.com/lucascolley/numpy"
# numpy.subdirectory = "pixi-packages/asan"
# numpy.rev = "29d042b3b3e79b0a26fd2695ee469c66f4e98266"numpy.path = "/Users/lucascolley/ghq/github.com/rgommers/pixi-dev-scipystack/numpy/numpy/pixi-packages/asan"~/sandbox/v3-panic via 🧚 v0.72.0tixi lockError: × failed to solve requirements of environment 'default' for platform 'osx-arm64' ├─▶ × failed to solve the environment ╰─▶ Cannot solve the request because of: numpy * cannot be installed because there are no viable options: └─ numpy 2.6.0.dev0 would require └─ python_abi 3.16.* *_asan_cp316, for which no candidates were found.~/sandbox/v3-panic via 🧚 v0.72.0 took 2stixi lockError: × failed to solve requirements of environment 'default' for platform 'osx-arm64' ├─▶ × failed to solve the environment ╰─▶ Cannot solve the request because of: numpy * cannot be installed because there are no viable options: └─ numpy 2.6.0.dev0 would require └─ python_abi 3.16.* *_tsan_cp316t, for which no candidates were found.

Extends the previous commit to the reporter's newer setup, verified
against the real reproduction (lucascolley/numpy@29d042b, lock now
succeeds and is stable across re-runs):
- extract_run_exports consults the exporting record's own `sources`
map first (anchored to that record's manifest). This covers recipe
sibling outputs — `python` weak-exports `python_abi`, which never
appears in the consumer's host env, so the env-wide lookup alone
cannot see it.
- A binary-shaped run dep on a package that was built from source in
the record's build/host envs (numpy's `python >=3.12` while the host
env holds source-built python) now registers that source too — the
package was linked against it, so the install-time env must build the
same source rather than fetch a channel package that conflicts with
the source python_abi's run constraint.
- The passthrough backend emits run-exports naming model-declared
source dependencies as source specs (mirroring real backends'
local_source_packages) so the regression test can model the sibling
case: package_b run-exports package_c, which is absent from
package_a's host env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wolfv

wolfv commented Jul 3, 2026

Copy link
Copy Markdown
MemberAuthor

Pushed a second commit after verifying against the real reproduction (the newer manifest at lucascolley/numpy@29d042b where python_abi is no longer an explicit host dependency). The first commit alone was not enough there, for two reasons:

  1. Sibling-output run-exports: python_abi is a sibling output of python's recipe and never appears in numpy's host env, so mapping exported names against the solved env's records can't see it. The link lives in python's own record sources map (the rattler-build backend emits sibling-output run-exports as source specs via local_source_packages). extract_run_exports now consults the exporting record's sources first, anchored to that record's manifest, with the env-wide lookup as fallback.
  2. Binary run deps on source-built host packages: numpy's python >=3.12 (from pixi-build-python) is a plain binary spec, so the top-level solve only considered channel pythons (3.12.x), which conflicted with the source python_abi's run constraint (python 3.16.* *_asan_cp316). A run dep whose name resolved to a source-built package in the record's build/host envs now registers that source too — the package was linked against it.

With both, pixi lock on the reproduction succeeds and re-locking is stable (Lock-file was already up-to-date, identical hash across runs). The regression test now also covers the sibling case (package_b run-exports package_c, which is absent from package_a's host env).

Remaining, filed mentally as a separate issue: flags don't constrain source-record variant selection. With python.flags = ["asan"], linux-64 locked the asan variant but osx-arm64 locked *_cp316t (freethreading, non-asan), and before the fix the reported missing variant flipped between runs (*_asan_cp316 / *_tsan_cp316t / *_cp316). The solver appears to pick freely among python variants regardless of the flags matchspec — that's why consecutive tixi lock runs named different variants.

@lucascolley

Copy link
Copy Markdown
Collaborator

The solver appears to pick freely among python variants regardless of the flags matchspec — that's why consecutive tixi lock runs named different variants.

then we should fix this too!

@lucascolley

Copy link
Copy Markdown
Collaborator

confirmed that it can still pick the wrong variant:

numpy conda /Users/lucascolley/ghq/github.com/rgommers/pixi-dev-scipystack/numpy/numpy/pixi-packages/asan
openssl 3.6.3 hd24854e_0 2.96 MiB conda https://prefix.dev/conda-forge
python 3.16 0_cp316 conda git+https://github.com/lucascolley/cpython?subdirectory=Tools%2Fpixi-packages&rev=8b5b0c29797cf88d78ef014916a5e5a5d51bbf95#8b5b0c29797cf88d78ef014916a5e5a5d51bbf95
python_abi 3.16 0_cp316 conda git+https://github.com/lucascolley/cpython?subdirectory=Tools%2Fpixi-packages&rev=8b5b0c29797cf88d78ef014916a5e5a5d51bbf95#8b5b0c29797cf88d78ef014916a5e5a5d51bbf95

…end recipe round-trip
A source dependency's selectors (version, build, build-number, extras,
flags, subdir, license, condition) were silently discarded twice on the
way through a generated recipe:
- source_package_spec_to_package_dependency put an empty MatchSpec next
to the encoded location URL,
- and the conversions back to a SourcePackageSpec
(from_source_matchspec_into_package_spec and convert_dependency's
source-url branch) rebuilt the spec from the URL alone. The
local-source-packages merge branch also skipped extras/flags/condition.
The visible symptom was `python = { git = ..., flags = ["asan"] }"
degrading to "any python variant from that source": the host env solve
picked a variant nondeterministically (asan on one run, tsan or
freethreading on the next), producing unstable and wrong locks
(prefix-dev#6482's flip-flopping error messages).
The selectors now ride on the SourceMatchSpec's inner MatchSpec — the
existing v3 detection (required_repodata_revision) already switches the
recipe parse to the V3 syntax surface when they are present, so
flags=[...] survives the YAML round-trip. All conversion sites use
exhaustive destructuring/construction so the compiler flags the next
field that gets added to SourcePackageSpec or MatchSpec instead of
silently widening the dependency.
Verified against the issue's reproduction with
PIXI_BUILD_BACKEND_OVERRIDE: both platforms now deterministically lock
python/python_abi 0_asan_cp316 (previously osx-arm64 locked the
freethreading variant and consecutive runs flipped variants).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wolfv

wolfv commented Jul 3, 2026

Copy link
Copy Markdown
MemberAuthor

Third commit: tracked down the variant nondeterminism (*_asan_cp316 / *_tsan_cp316t / *_cp316 flip-flopping between runs, and osx-arm64 locking the freethreading variant despite flags = [\"asan\"]).

Root cause: source dependencies lose all matchspec selectors (version, build, flags, extras, ...) on the round-trip through a generated recipe in pixi_build_backend:

  • source_package_spec_to_package_dependency paired the encoded location URL with an emptyMatchSpec (..Default::default()),
  • and both reconstruction paths (from_source_matchspec_into_package_spec, convert_dependency's source-url branch) rebuilt the SourcePackageSpec from the URL alone. The local_source_packages merge branch also skipped flags/extras/condition.

So python = { git = ..., flags = [\"asan\"] } reached the host env solve as just "python from that source", and the solver picked freely among the variant records — rattler's flags matching itself is fine, the constraint just never arrived.

The selectors now travel on the SourceMatchSpec's inner MatchSpec; the existing v3 detection (required_repodata_revision) already flips the recipe parse to the V3 syntax surface when they're present, so flags=[...] survives the YAML round-trip. All conversion sites now use exhaustive destructuring/construction (no ..Default::default() / field-poking), so the compiler flags the next selector field added to SourcePackageSpec or MatchSpec instead of silently widening the dependency — which is exactly the bug class this was.

Verified against the reproduction with PIXI_BUILD_BACKEND_OVERRIDE pointing at the in-repo pixi-build-python: both platforms now deterministically lock python/python_abi0_asan_cp316 (identical lockfile hash across from-scratch runs). Note the released pixi-build-python also predates the flags field on SourcePackageSpec, so user-visible behavior additionally needs a backend release once this merges.

@lucascolley

Copy link
Copy Markdown
Collaborator

this fixes the linked issue for me now 👍

@Hofer-JulianHofer-Julian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It feels like we are working around our current architecture here rather than fixing it. However, I don't have a better solution and this fixes the problem, so let's bring it in!

@Hofer-Julian
Hofer-Julian merged commit 1214299 into prefix-dev:mainJul 6, 2026
36 checks passed
@lucascolleylucascolley added bug Something isn't working area:build labels Jul 6, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:buildbugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(build): recursive source dependency not found

3 participants

@wolfv@lucascolley@Hofer-Julian