-
-
Notifications
You must be signed in to change notification settings - Fork 690
Fix uv PEX builder to use pex3 lock export #23227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
497c59f
0872e7b
7f50667
1559213
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,13 +85,13 @@ | |
| add_prefix, | ||
| create_digest, | ||
| digest_to_snapshot, | ||
| get_digest_contents, | ||
| merge_digests, | ||
| remove_prefix, | ||
| ) | ||
| from pants.engine.process import ( | ||
| Process, | ||
| ProcessCacheScope, | ||
| ProcessExecutionFailure, | ||
| ProcessResult, | ||
| execute_process_or_raise, | ||
| fallible_to_exec_result_or_raise, | ||
|
|
@@ -560,10 +560,13 @@ async def _build_uv_venv( | |
| uv_request.description, | ||
| ) | ||
|
|
||
| # Try to extract the full resolved package list from the lockfile | ||
| # so we can pass pinned versions with --no-deps (reproducible). | ||
| # Try to export the lockfile via `pex3 lock export` so we can pass pinned | ||
| # versions with --no-deps (reproducible). This uses Pex's stable CLI rather | ||
| # than parsing the internal lockfile JSON directly. | ||
| # Fall back to letting uv resolve transitively if no lockfile. | ||
| all_resolved_reqs: tuple[str, ...] = () | ||
| exported_reqs_digest: Digest | None = None | ||
| reqs_file = "pylock.toml" | ||
|
|
||
| if isinstance(uv_request.requirements, PexRequirements) and isinstance( | ||
| uv_request.requirements.from_superset, Resolve | ||
|
Comment on lines
570
to
571
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just reading words here - I don't know the Pants code - but "Pex requirements from superset Resolve" suggest you only need a subset of the locked resolve to be exported / venv'd / PEXed up. Pex supports this: # Export the full lock:
:; pex3 lock create ansible -o lock.json
:; pex3 lock export --format pep-751 -o pylock.full.toml lock.json
:; tomlq -r '.packages[] | .name + "==" + .version' pylock.full.toml
ansible==13.5
ansible-core==2.20.4
cffi==2
cryptography==46.0.6
jinja2==3.1.6
markupsafe==3.0.3
packaging==26
pycparser==3
pyyaml==6.0.3
resolvelib==1.2.1
# Export just a subset:
:; pex3 lock export-subset --format pep-751 -o pylock.subset.toml --lock lock.json cryptography
:; tomlq -r '.packages[] | .name + "==" + .version' pylock.subset.toml
cffi==2
cryptography==46.0.6
pycparser==3
# ~5ms hit for performing the subset resolve on the full lock to narrow it down to the transitive subset that covers `cryptography`:
:; hyperfine \
-w2 \
-n full \
'pex3 lock export --format pep-751 -o pylock.full.toml lock.json' \
-n subset \
'pex3 lock export-subset --format pep-751 -o pylock.subset.toml --lock lock.json cryptography'
Benchmark 1: full
Time (mean ± σ): 321.1 ms ± 2.5 ms [User: 296.5 ms, System: 24.6 ms]
Range (min … max): 317.9 ms … 326.2 ms 10 runs
Benchmark 2: subset
Time (mean ± σ): 326.5 ms ± 3.2 ms [User: 302.6 ms, System: 23.8 ms]
Range (min … max): 318.4 ms … 330.7 ms 10 runs
Summary
full ran
1.02 ± 0.01 times faster than subsetSo you may want to factor this in going forward. The savings may be in the margin, but even uv is slowed down a little bit if the full venv includes torch's transitive dependencies. If the PEX you're building needs some other portion of the venv, but none of the torch stuff - you just did a lot of extra work for nothing and this may show up on someones perf radar. I leave the experimentation / analysis to you all though. |
||
| ): | ||
|
|
@@ -573,42 +576,51 @@ async def _build_uv_venv( | |
| loaded_lockfile = await load_lockfile(LoadedLockfileRequest(lockfile), **implicitly()) | ||
| if loaded_lockfile.is_pex_native: | ||
| try: | ||
| digest_contents = await get_digest_contents(loaded_lockfile.lockfile_digest) | ||
| lockfile_bytes = next( | ||
| c.content for c in digest_contents if c.path == loaded_lockfile.lockfile_path | ||
| ) | ||
| lockfile_data = json.loads(lockfile_bytes) | ||
| all_resolved_reqs = tuple( | ||
| f"{req['project_name']}=={req['version']}" | ||
| for resolve in lockfile_data.get("locked_resolves", ()) | ||
| for req in resolve.get("locked_requirements", ()) | ||
| export_result = await fallible_to_exec_result_or_raise( | ||
| **implicitly( | ||
| PexCliProcess( | ||
| subcommand=("lock", "export"), | ||
| extra_args=( | ||
| "--format", | ||
| "pep-751", | ||
| "-o", | ||
| reqs_file, | ||
| loaded_lockfile.lockfile_path, | ||
| ), | ||
| additional_input_digest=loaded_lockfile.lockfile_digest, | ||
| description=f"Export lockfile for {uv_request.description}", | ||
| output_files=(reqs_file,), | ||
| ) | ||
| ) | ||
| ) | ||
| except (json.JSONDecodeError, KeyError, StopIteration) as e: | ||
| exported_reqs_digest = export_result.output_digest | ||
| except ProcessExecutionFailure as e: | ||
| logger.warning( | ||
| "pex_builder=uv: failed to parse lockfile for %s: %s. " | ||
| "pex_builder=uv: failed to export lockfile for %s: %s. " | ||
| "Falling back to transitive uv resolution.", | ||
| uv_request.description, | ||
| e, | ||
| ) | ||
| all_resolved_reqs = () | ||
|
|
||
| uv_reqs = all_resolved_reqs or uv_request.req_strings | ||
| use_exported_lockfile = exported_reqs_digest is not None | ||
|
|
||
| if all_resolved_reqs: | ||
| if use_exported_lockfile: | ||
| logger.debug( | ||
| "pex_builder=uv: using %d pinned packages from lockfile with --no-deps for %s", | ||
| len(all_resolved_reqs), | ||
| "pex_builder=uv: using exported lockfile with --no-deps for %s", | ||
| uv_request.description, | ||
| ) | ||
| assert exported_reqs_digest is not None | ||
| reqs_digest = exported_reqs_digest | ||
| else: | ||
| logger.debug( | ||
| "pex_builder=uv: no lockfile available, using transitive uv resolution for %s", | ||
| uv_request.description, | ||
| ) | ||
|
|
||
| reqs_file = "__uv_requirements.txt" | ||
| reqs_content = "\n".join(uv_reqs) + "\n" | ||
| reqs_digest = await create_digest(CreateDigest([FileContent(reqs_file, reqs_content.encode())])) | ||
| reqs_file = "__uv_requirements.txt" | ||
| reqs_content = "\n".join(uv_request.req_strings) + "\n" | ||
| reqs_digest = await create_digest( | ||
| CreateDigest([FileContent(reqs_file, reqs_content.encode())]) | ||
| ) | ||
|
|
||
| complete_pex_env = pex_env.in_sandbox(working_directory=None) | ||
| uv_cache_dir = ".cache/uv_cache" | ||
|
|
@@ -661,7 +673,7 @@ async def _build_uv_venv( | |
| os.path.join(_UV_VENV_DIR, "bin", "python"), | ||
| "-r", | ||
| reqs_file, | ||
| *(("--no-deps",) if all_resolved_reqs else ()), | ||
| *(("--no-deps",) if use_exported_lockfile else ()), | ||
| *downloaded_uv.args_for_uv_pip_install, | ||
| ) | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "pinned versions" you really mean "locked requirements". I think the language of "pined versions" - which is common parlance - led you astray on your earlier foray. In the world of Python ecosystem requirements, requirements you can pin (name + version) cover 1/4 of the landscape only. There's also VCS, source from non-sdist archives and source from local project directories. These latter 3 do not necessarily have a known version up front - you have to build them to find that out.