Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/notes/2.34.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ Fixed a bug that could cause tool execution to fail when resolving the tool from

Required named indexes to be referenced explicitly in requirement sources when using the uv resolver, to match the pex resolver's behavior.

Local `[python-repos].find_links` directories under the buildroot no longer tie `uv` lockfiles to one checkout: they are now recorded as buildroot-relative paths. Builds using wheels from such directories also now get the same cache keys across checkouts and machines.

Sidecar lockfile metadata files (`*.lock.metadata`, written when `[python].separate_lockfile_metadata_file` is enabled) now end with a trailing newline. The bundled tool metadata files have been updated accordingly, so regenerating them no longer produces a spurious diff.

Interpreter constraints can now select a specific CPython ABI, to distinguish the free-threaded (no-GIL) build from the standard build. Qualify the interpreter type using either a PEP 508 extra, `CPython[free-threaded]` or `CPython[gil]`, or the equivalent `CPython+t` / `CPython-t` spellings. Both spellings are adopted from Pex. For example, `interpreter_constraints=["CPython[free-threaded]==3.14.*"]` matches only free-threaded 3.14 interpreters, while `CPython==3.14.*` continues to match either ABI.
Expand Down
28 changes: 22 additions & 6 deletions src/python/pants/backend/python/goals/lockfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import itertools
import os.path
from collections import defaultdict
from dataclasses import dataclass
from dataclasses import dataclass, replace
from operator import itemgetter
from typing import cast

Expand Down Expand Up @@ -43,7 +43,12 @@
load_lockfile,
strip_comments_from_pex_json_lockfile,
)
from pants.backend.python.util_rules.uv import UvEnvironment, generate_pyproject_toml
from pants.backend.python.util_rules.uv import (
UvEnvironment,
generate_pyproject_toml,
relativize_local_find_links,
)
from pants.base.build_root import BuildRoot
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
from pants.core.goals.generate_lockfiles import (
DEFAULT_TOOL_LOCKFILE,
Expand Down Expand Up @@ -351,6 +356,7 @@ async def generate_uv_lockfile(
generate_lockfiles_subsystem: GenerateLockfilesSubsystem,
downloaded_uv: DownloadedUv,
uv_env: UvEnvironment,
buildroot: BuildRoot,
level: LogLevel,
) -> GenerateLockfileResult:
if not req.interpreter_constraints:
Expand All @@ -364,12 +370,18 @@ async def generate_uv_lockfile(
)
resolve_config.validate_for_uv(req.resolve_name)

resolve_find_links, resolve_local_find_links_globs = relativize_local_find_links(
resolve_config.find_links, buildroot.path
)
extra_find_links, extra_local_find_links_globs = relativize_local_find_links(
req.find_links, buildroot.path
)
pyproject_content = generate_pyproject_toml(
req.resolve_name,
req.interpreter_constraints,
req.requirements,
indexes=resolve_config.indexes,
find_links=(*resolve_config.find_links, *req.find_links),
find_links=(*resolve_find_links, *extra_find_links),
sources=resolve_config.sources,
)

Expand Down Expand Up @@ -408,7 +420,7 @@ async def generate_uv_lockfile(
# is generated.
pass

uv_config = resolve_config.uv_config()
uv_config = replace(resolve_config, find_links=resolve_find_links).uv_config()

uv_config_digest = await create_digest(
CreateDigest(
Expand All @@ -418,9 +430,13 @@ async def generate_uv_lockfile(
]
)
)

find_links_digest = await path_globs_to_digest(
PathGlobs((*resolve_local_find_links_globs.globs, *extra_local_find_links_globs.globs))
)
input_digest = await merge_digests(
MergeDigests([downloaded_uv.digest, uv_config_digest, existing_uv_lock_digest])
MergeDigests(
[downloaded_uv.digest, uv_config_digest, existing_uv_lock_digest, find_links_digest]
)
)

result = await execute_process_or_raise(
Expand Down
3 changes: 3 additions & 0 deletions src/python/pants/backend/python/util_rules/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,9 @@ async def _setup_pex_requirements(
concurrency_available,
)

if not reqs_info.req_strings:
return _BuildPexRequirementsSetup([], [], concurrency_available)

# We use pip to perform a normal resolve.
digests = []
argv = [
Expand Down
3 changes: 3 additions & 0 deletions src/python/pants/backend/python/util_rules/pex_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,9 @@ def assert_setup(
),
)

# Nothing to resolve, so no repository args that could make the cache key path-dependent.
assert_setup(PexRequirements(), _BuildPexRequirementsSetup([], [], 0))

# Pex lockfile.
assert_setup(
EntireLockfile(lockfile_obj, complete_req_strings=reqs),
Expand Down
47 changes: 43 additions & 4 deletions src/python/pants/backend/python/util_rules/uv.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
determine_resolve_config,
generate_uv_index_config,
)
from pants.base.build_root import BuildRoot
from pants.core.util_rules import system_binaries
from pants.core.util_rules.env_vars import environment_vars_subset
from pants.core.util_rules.subprocess_environment import SubprocessEnvironmentVars
Expand All @@ -47,11 +48,13 @@
CreateDigest,
FileContent,
MergeDigests,
PathGlobs,
)
from pants.engine.intrinsics import (
create_digest,
get_digest_contents,
merge_digests,
path_globs_to_digest,
)
from pants.engine.pants_lock import pants_lock_bin
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
Expand Down Expand Up @@ -93,6 +96,35 @@ def append_only_caches(cls) -> FrozenDict[str, str]:
return FrozenDict({cls.cache_name: cls.cache_dir})


def relativize_local_find_links(
find_links: Iterable[str], buildroot: str
) -> tuple[tuple[str, ...], PathGlobs]:
"""Rewrite find-links dirs under the buildroot to buildroot-relative paths.

uv records find-links paths verbatim in uv.lock, so absolute paths would tie the lockfile (and
every process that consumes it) to one checkout location. Returns the rewritten find-links and
the globs for the files of those dirs, which must be in the uv process sandbox.
"""
rewritten: list[str] = []
local_globs: list[str] = []
for find_link in find_links:
if find_link.startswith("file://"):
path = find_link[len("file://") :]
elif os.path.isabs(find_link):
path = find_link
else:
rewritten.append(find_link)
continue
path = os.path.normpath(path)
if os.path.isabs(path) and os.path.commonpath((path, buildroot)) == buildroot:
Comment thread
martvald marked this conversation as resolved.
Resolved
relative_dir = os.path.relpath(path, buildroot)
rewritten.append(relative_dir)
local_globs.append(os.path.join(relative_dir, "*"))
else:
rewritten.append(find_link)
return tuple(rewritten), PathGlobs(local_globs)


@dataclass(frozen=True)
class UvEnvironment:
env: FrozenDict[str, str]
Expand Down Expand Up @@ -189,6 +221,7 @@ async def create_venv_repository_from_uv_lockfile(
realpath_binary: RealpathBinary,
rm_binary: RmBinary,
maybe_flock_binary: MaybeFlockBinary,
buildroot: BuildRoot,
level: LogLevel,
) -> VenvRepository:
"""Install all packages from a uv lockfile into a virtualenv."""
Expand All @@ -209,6 +242,9 @@ async def create_venv_repository_from_uv_lockfile(
ResolveConfigRequest(metadata.resolve), **implicitly()
)

find_links, local_find_links_globs = relativize_local_find_links(
resolve_config.find_links, buildroot.path
)
pyproject_content = generate_pyproject_toml(
metadata.resolve,
metadata.valid_for_interpreter_constraints,
Expand All @@ -222,7 +258,7 @@ async def create_venv_repository_from_uv_lockfile(
# they don't involve auth, so they are not strictly needed after lockfile generation.
# TODO: Get rid of "extra find_links" entirely, and simply hard-code an explicit
# source for pants_requirements.
find_links=resolve_config.find_links,
find_links=find_links,
sources=resolve_config.sources,
)

Expand All @@ -238,8 +274,9 @@ async def create_venv_repository_from_uv_lockfile(
),
get_digest_contents(request.lockfile.lockfile_digest),
)
uv_lock_digest = await create_digest(
CreateDigest([FileContent("uv.lock", uv_lock_contents[0].content)])
uv_lock_digest, find_links_digest = await concurrently(
create_digest(CreateDigest([FileContent("uv.lock", uv_lock_contents[0].content)])),
path_globs_to_digest(local_find_links_globs),
)

input_digest = await merge_digests(
Expand All @@ -248,6 +285,7 @@ async def create_venv_repository_from_uv_lockfile(
downloaded_uv.digest,
uv_config_digest,
uv_lock_digest,
find_links_digest,
)
)
)
Expand All @@ -265,6 +303,7 @@ async def create_venv_repository_from_uv_lockfile(
entropy.update((request.uv_platform or "").encode())
entropy.update(b"\0")
entropy.update(input_digest.fingerprint.encode())
entropy.update(b"\0UV_NO_INSTALLER_METADATA=1")
venv_path_suffix = entropy.hexdigest()

uv_cmd = shlex.join(
Expand Down Expand Up @@ -343,7 +382,7 @@ async def create_venv_repository_from_uv_lockfile(
creation_subprocess=Subprocess(
command=command,
input_digest=input_digest,
env=uv_env.env,
env=FrozenDict({**uv_env.env, "UV_NO_INSTALLER_METADATA": "1"}),
append_only_caches={
**downloaded_uv.append_only_caches(),
**VenvRepository.append_only_caches(),
Expand Down
Loading
Loading