Skip to content

Add experimental uv resolver for Python lockfile generation#23212

Closed
Affanmir wants to merge 5 commits into
pantsbuild:mainfrom
Affanmir:feature/uv-lockfile-resolver
Closed

Add experimental uv resolver for Python lockfile generation#23212
Affanmir wants to merge 5 commits into
pantsbuild:mainfrom
Affanmir:feature/uv-lockfile-resolver

Conversation

@Affanmir

@Affanmir Affanmir commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds [python].lockfile_resolver = "uv" option (default: pex) for faster lockfile generation
  • Uses uv pip compile to pre-resolve pinned requirements, then pex lock create --no-transitive to materialize the PEX lockfile
  • Supports all lock styles including universal (via uv pip compile --universal)
  • Adds [uv].args_for_lockfile_resolve for passthrough flags (e.g., --index-strategy unsafe-first-match)

Motivation

Issue: #20679 — faster, more ergonomic lockfile generation by leveraging uv's PubGrub resolver.

This builds on the uv PEX builder from #23197 (merged) and is inspired by the approach in #22949, with key improvements:

Aspect PR #22949 This PR
Universal lock style Blocked (raises error) Supported via --universal
Conflicts with main Creates uv.py (already exists) Works with existing uv.py from #23197
Unrelated changes Includes Rust file edits Clean diff, Python only
Args passthrough [uv].args (general) [uv].args_for_lockfile_resolve (specific)
register.py Modifies No change needed

Benchmarks

Environment: Apple M4, macOS 15.x (Darwin 24.6.0), arm64
Dataset: 87 top-level requirements (Trio test deps + web/data/cloud ecosystem) → 169 resolved packages
Tool: hyperfine v1.20.0
Methodology: Same dataset structure as uv's own benchmarks (Trio project requirements)

Resolver-only comparison (no Pants overhead)

Isolates the dependency resolution step — uv pip compile vs pex lock create:

Scenario pex lock create (pip) uv pip compile Speedup
Cold cache 155.9s ± 10.8s 2.7s ± 0.1s 57.6x
Warm cache 50.0s ± 8.3s 0.051s ± 0.004s 975x

End-to-end Pants comparison

Full pants generate-lockfiles --resolve=... (includes engine init, interpreter discovery, PEX CLI bootstrap):

Scenario lockfile_resolver = "pex" lockfile_resolver = "uv" Speedup
Semi-warm (wheel caches populated) 35.9s ± 13.7s 24.3s ± 0.8s 1.48x

Note: End-to-end improvement is smaller because Pants engine overhead (~20s) is constant. The uv resolver also shows much lower variance (±0.8s vs ±13.7s), providing more consistent lockfile generation times.

Correctness

Both resolvers produce equivalent lockfiles — 168/169 packages identical, with minor differences in optional/conditional dependency selection (greenlet vs tomli), which is expected behavior between different resolvers.

Reproduction

# Direct resolver comparison (cold)
hyperfine --runs 3 \
  --prepare "rm -rf /tmp/pex-bench-cache /tmp/uv-bench-cache" \
  -n "pex lock create" \
    "PEX_ROOT=/tmp/pex-bench-cache pex3 lock create --style=universal --target-system linux --target-system mac --interpreter-constraint 'CPython==3.11.*' --output=/tmp/out.json -r requirements.txt" \
  -n "uv pip compile" \
    "UV_CACHE_DIR=/tmp/uv-bench-cache uv pip compile requirements.txt --output-file /tmp/out.txt --python-version 3.11 --no-header --no-annotate"

# End-to-end Pants comparison (from pants source checkout with this PR)
hyperfine --runs 3 \
  --prepare "rm -rf ~/.cache/pants/lmdb_store && rm -f path/to/lockfile" \
  -n "pex" "./pants --no-pantsd --python-lockfile-resolver=pex generate-lockfiles --resolve=..." \
  -n "uv"  "./pants --no-pantsd --python-lockfile-resolver=uv generate-lockfiles --resolve=..."

Disclaimers

  • Results are environment-specific; performance varies by hardware, OS, and network
  • "Cold" clears process/resolver caches; "semi-warm" retains downloaded wheel caches to isolate resolver speed from network I/O
  • --no-pantsd used for all measurements (no daemon memoization)
  • The end-to-end Pants test runs from source (includes Rust engine compilation overhead not present in released binaries)

How it works

requirements (from targets)
        │
        │  [python].lockfile_resolver = "uv"
        ▼
  uv pip compile [--universal] [--python-version X.Y]
        │
        ▼
  pinned requirements.txt
        │
        │  pex lock create --no-transitive --style={universal,strict,sources}
        ▼
  PEX lockfile (same format as today)

Usage

[python]
enable_resolves = true
lockfile_resolver = "uv"  # opt-in

[uv]
args_for_lockfile_resolve = ["--index-strategy", "unsafe-first-match"]

Current limitations

Limitation Reason
No complete_platforms Not yet wired through uv step
No per-resolve overrides/sources/excludes Avoids silently changing semantics
Non-universal lock styles require single Python major.minor uv resolves for exactly one target interpreter

Code changes

File Change
subsystems/setup.py LockfileResolver enum + lockfile_resolver option
subsystems/uv.py args_for_lockfile_resolve passthrough + updated DownloadedUv
goals/lockfile.py uv pre-resolve pipeline in generate_lockfile()
goals/lockfile_test.py 6 new tests (validation + integration)
lockfiles.mdx Documentation section
2.32.x.md Release note

Test plan

  • test_strip_named_repo — unit test for index URL stripping helper
  • test_uv_resolver_rejects_complete_platforms — validates error on unsupported complete_platforms
  • test_uv_resolver_strict_requires_single_python — validates error on multi-version strict
  • test_uv_resolver_rejects_overrides — validates error on unsupported overrides
  • test_uv_resolver_strict_generates_valid_lockfile — E2E: generates valid PEX lockfile with uv + strict
  • test_uv_resolver_universal_generates_valid_lockfile — E2E: generates valid PEX lockfile with uv + universal
  • All existing lockfile tests pass unchanged

AI disclosure

This PR was created with extensive use of Claude Code (Anthropic's CLI agent). Claude explored the Pants codebase, designed the implementation approach, wrote the code/tests/docs, and ran the benchmarks. All changes were reviewed and test runs verified by the author.

🤖 Generated with Claude Code

Affanmir and others added 2 commits April 1, 2026 20:47
Add `[python].lockfile_resolver = "uv"` option that uses `uv pip compile`
to pre-resolve pinned requirements before running `pex lock create --no-transitive`.
This provides 3-4x faster lockfile generation on cold cache by leveraging
uv's PubGrub resolver. All lock styles are supported including `universal`
(via `uv pip compile --universal`).

Closes pantsbuild#20679

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@benjyw

benjyw commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Hi @Affanmir, thanks for this contribution.

I have some questions about the background, motivation, and creation of this change. It is unusual to receive a PR that directly competes with an existing, pending PR by another contributor that is already going through code review. We like to keep things collegial and friendly here, and so in this case it might have been appropriate to first discuss your change with the author of that PR, and with the maintainers of the project.

It would be helpful if you could jump on Slack, introduce yourself there, and give us a bit of background about your use of Pants, your motivation for this change, and how you created it using Claude.

Thanks!

@Niccolum

Niccolum commented Apr 9, 2026

Copy link
Copy Markdown

Hi!

As far as I can see, this PR solves all the issues related to UV for Pants. Can you tell me if there's anything I can do to help make sure these changes make it to the main branch?

@benjyw

benjyw commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Hi!

As far as I can see, this PR solves all the issues related to UV for Pants. Can you tell me if there's anything I can do to help make sure these changes make it to the main branch?

We are still reviewing this change, and considering whether this is the way to go WRT uv integration. We intend to get either something along the lines of this PR, or something that uses a different approach, merged soon.

@benjyw

benjyw commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Hi!
As far as I can see, this PR solves all the issues related to UV for Pants. Can you tell me if there's anything I can do to help make sure these changes make it to the main branch?

We are still reviewing this change, and considering whether this is the way to go WRT uv integration. We intend to get either something along the lines of this PR, or something that uses a different approach, merged soon.

We are going to discuss this at the monthly Pants developer meeting on Monday, which is a huddle in the #development channel on Slack, this Monday (4/13) at 1 PM PST, and all are welcome to join and participate!

@wisechengyi

wisechengyi commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

will take a look this week in conjunction with our internal needs.

@wisechengyi

wisechengyi commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Hi @Affanmir , thanks for the contribution! The implementation looks reasonable to me.

Although for our specific needs, --universal is too permissive, from performance perspective, it can be wasteful, and from functional POV, not all wheels can be resolved in the universal sense.

So we would ask for uv to handle a matrix of, for example,

python_versions = [3.10, 3.11]
platforms = ["x86_64-manylinux_2_34", "aarch64-apple-darwin"]

I don't think uv handles that natively, so this will become a 2x2=4 uv calls, and then we need to synthesize the result.

@benjyw how do you feel about it? I am fine landing this one first, and then we can add our ask later. It should be complimentary/additive to this implementation.

@benjyw

benjyw commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Hi @Affanmir , thanks for the contribution! The implementation looks reasonable to me.

Although for our specific needs, --universal is too permissive, from performance perspective, it can be wasteful, and from functional POV, not all wheels can be resolved in the universal sense.

So we would ask for uv to handle a matrix of, for example,

python_versions = [3.10, 3.11]
platforms = ["x86_64-manylinux_2_34", "aarch64-apple-darwin"]

I don't think uv handles that natively, so this will become a 2x2=4 uv calls, and then we need to synthesize the result.

@benjyw how do you feel about it? I am fine landing this one first, and then we can add our ask later. It should be complimentary/additive to this implementation.

I'm not sure how we would synthesize the result (which is a fully-pinned requirements.txt). And for uv pip compile the --python-version is a lower bound, so you would not need python versions in the matrix anyway.

If we were using uv lock then the lockfile is always universal, in that it captures the available metadata for all markers. True, it doesn't guarantee that a package will later install on a certain platform (there may be no wheel for a given platform, and/or the sdist may not build on that platform). But this is not an issue at lockfile creation time. uv pip compile is not as well documented, but it seems reasonable to hypothesize that it's similar, since I have to assume that it's using uv lock under the hood. We'd need to check all this of course. But if so, what is the issue with --universal ? Do you have evidence of a performance issue?

@wisechengyi

Copy link
Copy Markdown
Contributor

I'm not sure how we would synthesize the result (which is a fully-pinned requirements.txt). And for uv pip compile the --python-version is a lower bound, so you would not need python versions in the matrix anyway.

If we were using uv lock then the lockfile is always universal, in that it captures the available metadata for all markers. True, it doesn't guarantee that a package will later install on a certain platform (there may be no wheel for a given platform, and/or the sdist may not build on that platform). But this is not an issue at lockfile creation time. uv pip compile is not as well documented, but it seems reasonable to hypothesize that it's similar, since I have to assume that it's using uv lock under the hood. We'd need to check all this of course. But if so, what is the issue with --universal ? Do you have evidence of a performance issue?

Ah I see. Thanks!

uv's output is just a requirements.txt, so perf impact should be small (if not constraining python versions and platforms)

The potential bigger perf impact is around pex turning the requirements.txt into the pex lock, iiuc it is going to download all applicable linux and mac wheels, which is more than ["x86_64-manylinux_2_34", "aarch64-apple-darwin"]

Ref:

❯ will pex genererate the pex lock for all platforms and python versions? how does relate to --universal?                                                                                     

  Searched for 1 pattern (ctrl+o to expand)

● Short answer: the universality scope is parallel between the two tools, but uv pins versions while PEX enumerates artifacts.

  uv's job (resolution):
  - --universal (lockfile.py:163-164) → uv picks one pinned version per package that works across the whole interpreter-constraint range and all platforms. That's what lets a single requirements.txt feed PEX.
  - Non-universal (strict/sources) → requires exactly one major.minor (lockfile.py:310-320); uv pins for just that Python.

  PEX's job (artifact enumeration): Even with --no-transitive --requirement=<uv output>, PEX still hits indexes to record hashes/URLs for each (platform × python) combination into the PEX lock. The scope is
  controlled by --style + --target-system + interpreter constraints (lockfile.py:246-265, 386-421):
  - lock_style="universal" → --target-system=linux --target-system=mac plus the full IC range. PEX records wheels/sdists for linux+mac across every Python in IC.
  - lock_style="strict"/"sources" without complete_platforms → no --target-system, PEX locks only for the current platform.
  - complete_platforms set → PEX uses those platform JSONs instead, and IC is suppressed (lockfile.py:411-415). This is rejected when uv is enabled (lockfile.py:221-225).

  So "universal" means the same thing in both stages — uv resolves once across the range, PEX then fans out artifact entries for linux+mac × IC.

❯ what is IC?  IC range                                                                                                                                                                                           

● IC = Interpreter Constraints — the Python-version range a resolve is allowed to run under (e.g. CPython>=3.9,<3.13).

  In this codebase: InterpreterConstraints (lockfile.py:21, 85), sourced from [python].interpreter_constraints or [python].resolves_to_interpreter_constraints[<resolve>].

  "IC range" = the set of Python major.minor versions that satisfy those constraints (e.g. 3.9, 3.10, 3.11, 3.12). For a universal lock, uv resolves one version per package that works across that whole range,
  and PEX records artifacts for every Python in the range × each target platform.

❯ is IC range configurable in this case?                                                                                                                                                                          

  Searched for 1 pattern (ctrl+o to expand)

● Yes — two configurable inputs shape the IC range:

  1. [python].interpreter_constraints (or [python].resolves_to_interpreter_constraints[<resolve>]) — the constraint itself (e.g. ">=3.9,<3.13"), consumed at lockfile.py:581-585.
  2. [python].interpreter_versions_universe (setup.py:161) — the set of major.minor versions Pants considers candidates when expanding those constraints. Default is all versions Pants supports; narrow it to
  shrink the range.

  How each lock_style uses them under uv:
  - universal (lockfile.py:303-307): only the minimum version in the range is passed to uv as --python-version. PEX then fans artifacts across the full IC × universe.
  - strict / sources (lockfile.py:310-320): the IC must partition to exactly one major.minor within the universe, or uv errors out. So effectively the IC range must be a single Python version.

❯ how about platform constraint when pex turns requirements.txt into a pex lock?                                                                                                                                  

  Read 1 file (ctrl+o to expand)

● When uv feeds PEX, the platform knobs are reduced to two hardcoded paths (lockfile.py:220-265):

  - complete_platforms — rejected outright. uv errors before PEX is invoked (lockfile.py:221-225). This is the usual "lock for these exact platforms" knob in Pants, and it's unavailable under uv.
  - lock_style="universal" → PEX gets --target-system=linux --target-system=mac, hardcoded. Windows is deliberately excluded. Not configurable.
  - lock_style="strict" or "sources" → no --target-system passed; PEX locks for the current machine's platform only (whatever OS/arch runs generate-lockfiles). Also not configurable.

  So under uv today, the only platform surface is: (a) universal = linux+mac fixed, or (b) strict/sources = "whatever box ran this." If you need per-resolve platform control, you'd have to switch that resolve
  back to the PEX resolver and use resolves_to_complete_platforms.

❯ are linux and x86_64-manylinux_2_34 separete, or the former includes the latter?                                                                                                                                

● They're different granularities, and linux includes x86_64-manylinux_2_34.

  - --target-system=linux is a broad OS-family filter PEX uses during universal resolution. It tells PEX "don't prune wheels that could run on some Linux" — it spans all arches (x86_64, aarch64, …) and all
  manylinux/musllinux variants.
  - x86_64-manylinux_2_34 (as seen in a complete-platform JSON or wheel tag) is a specific ABI point: one arch + one glibc floor + one Python ABI. It's one of many points linux covers.

  So in a universal lock built with --target-system=linux, PEX records artifacts for whichever Linux wheel tags exist on the index (typically several manylinux variants plus sdist fallback), not just 2_34. If
  you need to pin to exactly x86_64-manylinux_2_34, that's complete_platforms territory — which, again, this uv path rejects.

❯ so if i only want x86_64-manylinux_2_34, using 'linux' can be wasteful, right? since pex is going to download all the applicable linux wheels                                                                   

● Yes — in two ways, though the cost shape matters:

  1. Lock size / creation time: with --target-system=linux PEX records artifacts for every Linux wheel tag available on the index (x86_64 manylinux_2_17/2_28/2_34, aarch64, musllinux, sdists, etc.). Each
  becomes a locked_requirements entry with its own URL and hash. More artifacts to fetch metadata for at lock time and a larger lockfile on disk.
  2. Resolution surface: uv's universal resolver must also pick a version that satisfies all those tags (including, say, an aarch64-only constraint). Occasionally this forces an older pin than x86_64-manylinux
  alone would need.

  Lock-consumption time isn't wasteful — when you later build a PEX for a specific target, PEX only downloads the matching artifact.

  The lean answer is complete_platforms with an x86_64-manylinux_2_34 JSON — which is exactly what this PR disallows under uv (lockfile.py:221-225). So today, under the uv resolver, you eat the linux+mac
  fan-out or you switch that resolve back to the PEX resolver.

@wisechengyi

Copy link
Copy Markdown
Contributor

So point is that I think we can land this, but may need to follow up on adding a bit more platform constraints to help with perf.

@jsirois

jsirois commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

So point is that I think we can land this, but may need to follow up on adding a bit more platform constraints to help with perf.

That may be, but also the LLM BS is BS. Back in October the 2.64.0 release had Pex defaulting to 0 downloads for modern Pips and indexes like PyPI that serve PEP-658 distribution metadata .

It would be really awesome if Pants folks stopped arm-chairing and actually got their hands dirty and did real work to suss all this out.

@wisechengyi FWIW, in your dialogue with the LLM, the LLM gets several other facts wrong. This is a super poor basis for decision making. Even without LLMs developers tend to be pro hand-wavers. You, traditionally, have not been one of these; so I find this worrying!

@seungwoo-ji-03

seungwoo-ji-03 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Was following this PR since it's related to #23197 and ran some benchmarks. Turns out the bottleneck is pex's artifact enumeration, not the resolve step.

Setup: 49 top-level requirements (requests, django, fastapi, boto3, pandas, etc.) → 137 resolved packages, --style=universal --target-system linux --target-system mac, CPython==3.11.*, uv 0.10.1, pex 2.93.0, hyperfine 1.20.0, macOS ARM64.

--no-transitive barely helps — ~98% of pex's time is artifact enumeration (wheel metadata/hash collection), not resolution:

pex3 lock create (warm):                 28.7s
pex3 lock create --no-transitive (warm): 28.2s  → only 0.5s saved

Cold cache (3 runs, cache cleared before each):

Approach Time vs baseline
pex3 lock create 43.4s ± 1.0s 1x
This PR: uv compilepex --no-transitive 44.1s ± 1.2s 1.0x
uv pip compile --generate-hashes (bypass pex) 671ms ± 72ms 65x
uv lock + uv export (bypass pex) 574ms ± 12ms 76x

Warm cache (3 runs, warmup 1):

Approach Time vs baseline
pex3 lock create 29.3s ± 0.2s 1x
This PR: uv compilepex --no-transitive 29.9s ± 0.7s 1.0x
uv pip compile --generate-hashes (bypass pex) 67ms ± 3ms 437x
uv lock + uv export (bypass pex) 99ms ± 2ms 296x

Both uv approaches resolve the same 137 packages at identical versions. Output is requirements.txt with hashes — Pants already supports consuming this format via --requirement + --no-transitive in _setup_pex_requirements.

Reproduction:

hyperfine --runs 3 --warmup 1 \
  -n "pex3 lock create" \
    "pex3 lock create --style=universal --target-system linux --target-system mac \
     --interpreter-constraint 'CPython==3.11.*' -r requirements.txt -o out.json" \
  -n "this PR: uv+pex" \
    "uv pip compile requirements.txt --python-version 3.11 --no-header --universal -o pinned.txt \
     && pex3 lock create --style=universal --target-system linux --target-system mac \
     --interpreter-constraint 'CPython==3.11.*' --no-transitive -r pinned.txt -o out.json" \
  -n "bypass pex: uv pip compile" \
    "uv pip compile requirements.txt --python-version 3.11 \
     --generate-hashes --no-header --universal -o out.txt"

The real speedup comes from skipping pex3 lock create entirely. Pants' consumption path already handles requirements.txt lockfiles with no changes needed.

@jsirois

jsirois commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Yes. Thanks for the perf breakdown. Unfortunately, that was a bit of a waste of your time. This has been known for a good while now, but Pants folks have been super poor about getting a hold of the tangle of issues, PRs and slack threads. I incessantly hound them, which they don't respond well (or at all) to. I'm just pointing out yet again that your time is being wasted by Pants maintainers not gathering threads, making a plan and announcing it.

@seungwoo-ji-03, for example, are you aware your merged PR may have been wasted effort itself?: https://pantsbuild.slack.com/archives/C0D7TNJHL/p1777401294195339?thread_ts=1777401294.195339&cid=C0D7TNJHL

@seungwoo-ji-03

seungwoo-ji-03 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

@jsirois Thanks for pointing this out — I wasn't aware of benjyw's pending work.
It'd be helpful if related PRs were flagged or closed with a note when a replacement is in progress, so contributors can avoid duplicating effort.

@benjyw

benjyw commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Yes, sorry, @seungwoo-ji-03, I should have been much more public about this work (which I started after #23197 was merged, after discussion at the recent developer monthly call). Just got a lot going on, and trying to get this work done in the time cracks between everything else.

That said, I have a mostly-working implementation of uv lockfile support (inspired by this PR and by your #23197) that doesn't require the intermediate conversions and pex invocations, and creates pexes as needed from a uv-synced venv. I'm ironing out an issue with console scripts, and then should have it ready for review, hopefully today.

@benjyw

benjyw commented May 1, 2026

Copy link
Copy Markdown
Contributor

Note that this is superseded by the more comprehensive solution in #23302. Thanks for all the effort on this front, and for the prodding to finally get this done properly.

benjyw added a commit that referenced this pull request May 5, 2026
uv lockfiles are first-class entities that users check in.
When a pex is needed, we use uv to create a venv, which
it does very efficiently, and then use pex's
`--venv-repository` feature to build a pex from
(a subset of) that venv's requirements.

Note that we still create VenvPexes to run tools, and
in some cases this might be unnecessary, since we
already have the uv venv. But utilizing those venvs 
directly would be an even bigger change (and this one
is already massive), and there are caching tradeoffs
to figure out . So we leave that for the future (and
probably for the new python  backend).

Note also that the dynamics of lockfile generation remain
pants's (you must generate explicitly) and not uv's
(lockfiles are automatically updated for you when
input requirements change). The latter is desirable, but
again this is for the future.

The general thrust of the changes is:

- Add uv as a third lockfile format (after pex and the deprecated
  legacy constraints file "locking")
- Update the lockfile metadata to include the format (and
  also the name of the resolve that created the lockfile).
- Make lockfile metadata reading much more robust 
  and useful, so we can get the format from it reliably.
  Unfortunately this logic is tricky since it has to handle
  pex lockfiles both with and without separate metadata files.
  For uv we simply require separate metadata files and do
  not add any metadata inside the lockfile itself, as we
  foolishly did for pex lockfiles at one point.
- Update the lockfile diffing code to support diffing
  across all combinations of old/new and pex/uv.
- Update the existing `uv` subsystem for more general use.
- Add src/python/pants/backend/python/util_rules/uv.py, 
  which runs `uv`, first generating ephemeral pyproject.toml
  and uv.toml config files. We handle environments correctly, 
  which requires some `realpath` shenanigans.
- Update some iffy logic in PythonToolBase that parses lockfiles
  to find useful info for help messages. It was previously relying
  on pex lockfile internals (which are not guaranteed to be stable)
and it now also relies on uv lockfile internals (similarly not stable).
  However now at least it fails gracefully if the lockfile parsing fails
   and just prints a less useful error message.
- Use all this in src/python/pants/backend/python/util_rules/pex.py, 
  instead of the previous, uv pex building solution.

Note that this supersedes the more limited (but much appreciated)
change in #23197 that implemented only the uv venv -> pex part.
It also supersedes the competing pending changes in #22949 
and #23212, which implemented partial support for uv resolution.
@benjyw

benjyw commented May 5, 2026

Copy link
Copy Markdown
Contributor

Closing since #23302 is now merged. We'd love your feedback on that one (I do have your first round of fixes, and will look at how to apply them). Thanks!

@benjyw benjyw closed this May 5, 2026
benjyw added a commit that referenced this pull request May 6, 2026
…ick of #23302) (#23320)

uv lockfiles are first-class entities that users check in.
When a pex is needed, we use uv to create a venv, which
it does very efficiently, and then use pex's
`--venv-repository` feature to build a pex from
(a subset of) that venv's requirements.

Note that we still create VenvPexes to run tools, and
in some cases this might be unnecessary, since we
already have the uv venv. But utilizing those venvs 
directly would be an even bigger change (and this one
is already massive), and there are caching tradeoffs
to figure out . So we leave that for the future (and
probably for the new python  backend).

Note also that the dynamics of lockfile generation remain
pants's (you must generate explicitly) and not uv's
(lockfiles are automatically updated for you when
input requirements change). The latter is desirable, but
again this is for the future.

The general thrust of the changes is:

- Add uv as a third lockfile format (after pex and the deprecated
  legacy constraints file "locking")
- Update the lockfile metadata to include the format (and
  also the name of the resolve that created the lockfile).
- Make lockfile metadata reading much more robust 
  and useful, so we can get the format from it reliably.
  Unfortunately this logic is tricky since it has to handle
  pex lockfiles both with and without separate metadata files.
  For uv we simply require separate metadata files and do
  not add any metadata inside the lockfile itself, as we
  foolishly did for pex lockfiles at one point.
- Update the lockfile diffing code to support diffing
  across all combinations of old/new and pex/uv.
- Update the existing `uv` subsystem for more general use.
- Add src/python/pants/backend/python/util_rules/uv.py, 
  which runs `uv`, first generating ephemeral pyproject.toml
  and uv.toml config files. We handle environments correctly, 
  which requires some `realpath` shenanigans.
- Update some iffy logic in PythonToolBase that parses lockfiles
  to find useful info for help messages. It was previously relying
  on pex lockfile internals (which are not guaranteed to be stable)
and it now also relies on uv lockfile internals (similarly not stable).
  However now at least it fails gracefully if the lockfile parsing fails
   and just prints a less useful error message.
- Use all this in src/python/pants/backend/python/util_rules/pex.py, 
  instead of the previous, uv pex building solution.

Note that this supersedes the more limited (but much appreciated)
change in #23197 that implemented only the uv venv -> pex part.
It also supersedes the competing pending changes in #22949 
and #23212, which implemented partial support for uv resolution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants