Skip to content

Support multiplatform pexes with uv lockfiles. - #23427

Merged
benjyw merged 10 commits into
pantsbuild:mainfrom
benjyw:multiplatform_pex_with_uv
Aug 10, 2026
Merged

Support multiplatform pexes with uv lockfiles.#23427
benjyw merged 10 commits into
pantsbuild:mainfrom
benjyw:multiplatform_pex_with_uv

Conversation

@benjyw

@benjyw benjyw commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Also supports building FaaS (awslambda/gcp cloud functions).

We leverage the fact that uv can create foreign-platform venvs
with the --python-platform flag. It still needs a local python
interpreter of the appropriate version, but it downloads and installs
wheels for the foreign platform. Of course there must be foreign
platform wheels available for all distributions. If there isn't one for
some dist, uv will build from the sdist for the local platform, which
may lead to a later runtime error.

Will close #23339

@benjyw
benjyw marked this pull request as draft June 15, 2026 20:19
@benjyw
benjyw force-pushed the multiplatform_pex_with_uv branch 7 times, most recently from 968088e to 80ad542 Compare June 20, 2026 14:26


@pytest.fixture
def complete_platform(rule_runner: PythonRuleRunner) -> bytes:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This fixture was unused even before my change.

@benjyw
benjyw force-pushed the multiplatform_pex_with_uv branch 3 times, most recently from d7c208c to 5414bb5 Compare June 21, 2026 16:17

@pytest.mark.platform_specific_behavior
@pytest.mark.parametrize("resolver", ["pex", "uv"])
def test_create_lambda_from_lockfile(resolver: str, rule_runner: PythonRuleRunner) -> None:

@benjyw benjyw Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Other tests were not using lockfiles but rather the legacy "resolve on demand without a lockfile" functionality. So this test is novel even in the pex case.

extra_log_lines_base = tuple() if complete_platforms_target_type else (" Runtime: python37",)
expected_extra_log_lines = extra_log_lines_base + (
expected_extra_log_lines = (
" Runtime: python37",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Now we always compute the runtime python version, even if a complete_platforms is specified, because we don't yet know if we're using pex or uv.

@benjyw
benjyw force-pushed the multiplatform_pex_with_uv branch 2 times, most recently from ebf6a00 to 1cc2bb3 Compare June 22, 2026 02:25
Comment thread docs/notes/2.33.x.md Outdated

Support for creating multiplatform/foreign platform pexes when using the `uv` resolver. This includes support for FAAS (AWS Lambda/Google Cloud Functions).

Support for remote caching/execution when using the `uv` resolver.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was in a previous PR that was not in the release notes.

reexported_handler_module=PythonAwsLambdaHandlerField.reexported_handler_module,
)
),
**implicitly(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Needed because we added a PythonSetup arg to the callee

target_name="unused",
runtime=TestRuntimeField("completely ignored!", address),
architecture=FaaSArchitecture.ARM64, # ignored
runtime=TestRuntimeField("3.45", address),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is no longer ignored - it's used to compute uv_platform, even if that doesn't end up being used (because we're using pex resolution).

@benjyw
benjyw force-pushed the multiplatform_pex_with_uv branch from 1cc2bb3 to f582492 Compare June 22, 2026 03:32
@benjyw

benjyw commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

AI disclosure: Claude was used to help with some plumbing, but it was mostly unhelpful with my attempts to get it to be smarter.

@benjyw
benjyw marked this pull request as ready for review June 22, 2026 03:51
@benjyw
benjyw requested review from cburroughs and sureshjoshi June 22, 2026 03:51
@benjyw benjyw added release-notes:not-required [CI] PR doesn't require mention in release notes and removed release-notes:not-required [CI] PR doesn't require mention in release notes labels Jun 26, 2026

@tobni tobni 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.

Every FaaS build materializes the entire resolve for that platform, so any partial-platform package anywhere in the resolve blocks unrelated targets.

Concretely for us: we build desktop apps for macOS & intel linux and lambdas for AL2023, and a lambda whose closure is 23 pure-python requirements fails to package because pyqt6-qt6 has no wheel for the lambda platform.

Pex just did "the right thing" by subsetting the lock per target. Here we'd need to enumerate platform markers in our requirements.txt so that install-time skipping kicks in. Workable, but users may need to be informed requirements.txt markers is how to get a build working.

Future:
Could the sync be restricted to the consumer's closure? uv.lock knows the package.dependencies graph and pants already knows the target's top-level requirements, so the complement could be excluded via --no-install-package at the cost of keying venvs by closure rather than by resolve+platform? Might be complicated but it would give us feature parity.

Comment thread src/python/pants/backend/python/util_rules/faas.py Outdated
@tobni

tobni commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Another observation: (the empty uv.toml is on main, and #23454 only fixes uv lock) FaaS packaging uses sync and sync needs the indices.

uv.lock stores registry urls without credentials, so uv sync needs the index config again. The venv rule writes an empty uv.toml, so generation works but every install 401s against our CodeArtifact index.

Fix that works for us:

resolve_config = await determine_resolve_config(
    ResolveConfigRequest(metadata.resolve), **implicitly()
)
# snip
FileContent("uv.toml", resolve_config.uv_config().encode()),

@tobni

tobni commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Will add that with my comments addressed using hacks I got this to run in our repo, which is exciting!

@benjyw

benjyw commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

I'll fix this up ASAP per your comments

@cburroughs

Copy link
Copy Markdown
Contributor

Thanks for the contribution. We've just branched for 2.33.x, so merging this pull request now will come out in 2.34.x, please move the release notes updates to docs/notes/2.34.x.md if that's appropriate.

@benjyw

benjyw commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Future: Could the sync be restricted to the consumer's closure? uv.lock knows the package.dependencies graph and pants already knows the target's top-level requirements, so the complement could be excluded via --no-install-package at the cost of keying venvs by closure rather than by resolve+platform? Might be complicated but it would give us feature parity.

#23365 is an attempt in this direction. You can pass --only-group to restrict to the group and its transitive deps, and we already know the non-transitive direct deps to put in that group.

@benjyw

benjyw commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Another observation: (the empty uv.toml is on main, and #23454 only fixes uv lock) FaaS packaging uses sync and sync needs the indices.

uv.lock stores registry urls without credentials, so uv sync needs the index config again. The venv rule writes an empty uv.toml, so generation works but every install 401s against our CodeArtifact index.

Fix that works for us:

resolve_config = await determine_resolve_config(
    ResolveConfigRequest(metadata.resolve), **implicitly()
)
# snip
FileContent("uv.toml", resolve_config.uv_config().encode()),

Does #23443 address this?

@tobni

tobni commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Another observation: (the empty uv.toml is on main, and #23454 only fixes uv lock) FaaS packaging uses sync and sync needs the indices.
uv.lock stores registry urls without credentials, so uv sync needs the index config again. The venv rule writes an empty uv.toml, so generation works but every install 401s against our CodeArtifact index.
Fix that works for us:

resolve_config = await determine_resolve_config(
    ResolveConfigRequest(metadata.resolve), **implicitly()
)
# snip
FileContent("uv.toml", resolve_config.uv_config().encode()),

Does #23443 address this?

No, the venv rule generate_pyproject_toml needs to be supplied indices and sources, I think.

@benjyw
benjyw force-pushed the multiplatform_pex_with_uv branch from 0f66277 to 780405e Compare August 6, 2026 16:37
@benjyw
benjyw requested a review from tobni August 6, 2026 16:57
@benjyw

benjyw commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@tobni this is ready for another look, and if you can try it out in your repo that would be great. The main thing not yet addressed is syncing just the closure of the needed requirements. That can be done separately.

@tobni

tobni commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Yep, this works!

I do think the closure feature is important to get in though. The uv_platforms override unblocks for our build specifically, but it is a lie about the runtime: it happens to be safe for our case only because nothing we use hard requires glibc > 2.34 (not getting into the details here).

Some findings while running:

  1. Explicit complete_platforms no longer short-circuits inference. On main, complete_platforms returned early from infer_runtime_platforms. On this branch inference continues so it can also compute uv_platforms, so a version with no runtime name now fails, including with the pex resolver, which arguably makes that feature worthless? Try e.g python 3.15 IC and a correct complete_platforms for some lambda runtime:
InvalidTargetException: In the 'python_aws_lambda_function' target: Could not find a known runtime for the
inferred Python version and machine architecture!
* Python version: (3, 15) [inferred from interpreter constraints]
* Machine architecture: x86_64
...
To fix, please: - specify `uv_platforms` for the given machine architecture,
  or specify a runtime that is known to Pants.

This is not a feature we use, mind you. Our IC and lambda runtimes are lockstep.

  1. complete_platforms is ignored by uv. A warning when you mix a uv-resolve with complete_platforms pointing to uv_platforms is likely much better UX. Otherwise users might "flip the resolve switch" and start netting MacOs pexes that used to be cross-platform.

@benjyw

benjyw commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author
  1. Explicit complete_platforms no longer short-circuits inference. On main, complete_platforms returned early from infer_runtime_platforms. On this branch inference continues so it can also compute uv_platforms, so a version with no runtime name now fails, including with the pex resolver, which arguably makes that feature worthless? Try e.g python 3.15 IC and a correct complete_platforms for some lambda runtime:
InvalidTargetException: In the 'python_aws_lambda_function' target: Could not find a known runtime for the
inferred Python version and machine architecture!
* Python version: (3, 15) [inferred from interpreter constraints]
* Machine architecture: x86_64
...
To fix, please: - specify `uv_platforms` for the given machine architecture,
  or specify a runtime that is known to Pants.

This is not a feature we use, mind you. Our IC and lambda runtimes are lockstep.

Yeah, this is a little tricky - we also now must infer an interpreter version instead of allowing the ambient interpreter.

I wonder if we should do the following heuristic: only change the behavior if PythonSetup.resolver is uv. This would fail in the case where you have a uv-generated lockfile, but then you changed the resolver back to pex and didn't regen the lockfile. But that seems like a pretty unlikely corner case. WDYT?

@tobni

tobni commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Yeah that heuristic would catch that error.

My thoughts

  • Why the global option instead of the resolve's lockfile format attr? Maybe option value is the better source of truth, I dont actually know.
  • The side effect where users are using complete_platforms for faas and switch to uv resolve are a bit scary. Maybe it is an unlikely use case? Like I mentioned, we dont/wont.

@benjyw

benjyw commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Yeah that heuristic would catch that error.

My thoughts

  • Why the global option instead of the resolve's lockfile format attr? Maybe option value is the better source of truth, I dont actually know.

The lockfile format is the best source of truth, but we don't know it here, and changing that would mean pulling a lot of code inside out, which I really don't want to do.

  • The side effect where users are using complete_platforms for faas and switch to uv resolve are a bit scary. Maybe it is an unlikely use case? Like I mentioned, we dont/wont.

We can detect this - if an explicit complete_platforms is provided then we refuse to infer a uv_platforms and fail later.

@benjyw

benjyw commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Alternatively maybe the way to kill two birds with one stone and say that you cannot use uv if there is an explicit complete_platforms. You must first get rid of it to signal "yes I really mean to use uv". It's slightly clunky but it is at least easy to articulate and implement, and it is only a gotcha when transitioning from pex to uv for the first time, and then only if you kept the complete_platforms around.

@benjyw
benjyw merged commit e2d3b7d into pantsbuild:main Aug 10, 2026
25 checks passed
@benjyw
benjyw deleted the multiplatform_pex_with_uv branch August 10, 2026 21:40
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.

Support building lambdas and multiplatform pexes with the uv resolver

3 participants