Skip to content

Commit fd29d27

Browse files
authored
refactor: change site_packages_symlinks to venv_symlinks (#2939)
This generalizes the ability to populate the venv directory by adding and additional field, `kind`, which tells which directory of the venv to populate. A symbolic constant is used to indicate which directory so that users don't have to re-derive the platform and version specific paths that make up the venv directory names. This follows the design described by #2156 (comment) This also changes it to a depset of structs to make it more forward compatible. A provider is used because they're slightly more memory efficient than regular structs. Work towards #2156
1 parent 0d203a9 commit fd29d27

File tree

8 files changed

+187
-108
lines changed

8 files changed

+187
-108
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ END_UNRELEASED_TEMPLATE
7070
`_test` target is deprecated and will be removed in the next major release.
7171
([#2794](https://github.com/bazel-contrib/rules_python/issues/2794)
7272
* (py_wheel) py_wheel always creates zip64-capable wheel zips
73+
* (providers) (experimental) {obj}`PyInfo.venv_symlinks` replaces
74+
`PyInfo.site_packages_symlinks`
7375

7476
{#v0-0-0-fixed}
7577
### Fixed
@@ -203,7 +205,7 @@ END_UNRELEASED_TEMPLATE
203205
please check the {obj}`uv.configure` tag class.
204206
* Add support for riscv64 linux platform.
205207
* (toolchains) Add python 3.13.2 and 3.12.9 toolchains
206-
* (providers) (experimental) {obj}`PyInfo.site_packages_symlinks` field added to
208+
* (providers) (experimental) `PyInfo.site_packages_symlinks` field added to
207209
allow specifying links to create within the venv site packages (only
208210
applicable with {obj}`--bootstrap_impl=script`)
209211
([#2156](https://github.com/bazelbuild/rules_python/issues/2156)).

python/features.bzl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ def _features_typedef():
3131
:::
3232
::::
3333
34-
::::{field} py_info_site_packages_symlinks
34+
::::{field} py_info_venv_symlinks
3535
36-
True if the `PyInfo.site_packages_symlinks` field is available.
36+
True if the `PyInfo.venv_symlinks` field is available.
3737
38-
:::{versionadded} 1.4.0
38+
:::{versionadded} VERSION_NEXT_FEATURE
3939
:::
4040
::::
4141
@@ -61,7 +61,7 @@ features = struct(
6161
TYPEDEF = _features_typedef,
6262
# keep sorted
6363
precompile = True,
64-
py_info_site_packages_symlinks = True,
64+
py_info_venv_symlinks = True,
6565
uses_builtin_rules = not config.enable_pystar,
6666
version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "",
6767
)

python/private/attributes.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ The order of this list can matter because it affects the order that information
260260
from dependencies is merged in, which can be relevant depending on the ordering
261261
mode of depsets that are merged.
262262
263-
* {obj}`PyInfo.site_packages_symlinks` uses topological ordering.
263+
* {obj}`PyInfo.venv_symlinks` uses topological ordering.
264264
265265
See {obj}`PyInfo` for more information about the ordering of its depsets and
266266
how its fields are merged.

python/private/common.bzl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def create_py_info(
378378
implicit_pyc_files,
379379
implicit_pyc_source_files,
380380
imports,
381-
site_packages_symlinks = []):
381+
venv_symlinks = []):
382382
"""Create PyInfo provider.
383383
384384
Args:
@@ -396,7 +396,7 @@ def create_py_info(
396396
implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files
397397
that a binary can choose to include.
398398
imports: depset of strings; the import path values to propagate.
399-
site_packages_symlinks: {type}`list[tuple[str, str]]` tuples of
399+
venv_symlinks: {type}`list[tuple[str, str]]` tuples of
400400
`(runfiles_path, site_packages_path)` for symlinks to create
401401
in the consuming binary's venv site packages.
402402
@@ -406,7 +406,7 @@ def create_py_info(
406406
necessary for deprecated extra actions support).
407407
"""
408408
py_info = PyInfoBuilder.new()
409-
py_info.site_packages_symlinks.add(site_packages_symlinks)
409+
py_info.venv_symlinks.add(venv_symlinks)
410410
py_info.direct_original_sources.add(original_sources)
411411
py_info.direct_pyc_files.add(required_pyc_files)
412412
py_info.direct_pyi_files.add(ctx.files.pyi_srcs)

python/private/flags.bzl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,12 @@ def _venvs_site_packages_is_enabled(ctx):
154154
flag_value = ctx.attr.experimental_venvs_site_packages[BuildSettingInfo].value
155155
return flag_value == VenvsSitePackages.YES
156156

157-
# Decides if libraries try to use a site-packages layout using site_packages_symlinks
157+
# Decides if libraries try to use a site-packages layout using venv_symlinks
158158
# buildifier: disable=name-conventions
159159
VenvsSitePackages = FlagEnum(
160-
# Use site_packages_symlinks
160+
# Use venv_symlinks
161161
YES = "yes",
162-
# Don't use site_packages_symlinks
162+
# Don't use venv_symlinks
163163
NO = "no",
164164
is_enabled = _venvs_site_packages_is_enabled,
165165
)

python/private/py_executable.bzl

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ load(":flags.bzl", "BootstrapImplFlag", "VenvsUseDeclareSymlinkFlag")
5454
load(":precompile.bzl", "maybe_precompile")
5555
load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
5656
load(":py_executable_info.bzl", "PyExecutableInfo")
57-
load(":py_info.bzl", "PyInfo")
57+
load(":py_info.bzl", "PyInfo", "VenvSymlinkKind")
5858
load(":py_internal.bzl", "py_internal")
5959
load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo")
6060
load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
@@ -543,6 +543,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
543543
VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES
544544
)
545545
recreate_venv_at_runtime = False
546+
bin_dir = "{}/bin".format(venv)
546547

547548
if not venvs_use_declare_symlink_enabled or not runtime.supports_build_time_venv:
548549
recreate_venv_at_runtime = True
@@ -556,7 +557,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
556557
# When the venv symlinks are disabled, the $venv/bin/python3 file isn't
557558
# needed or used at runtime. However, the zip code uses the interpreter
558559
# File object to figure out some paths.
559-
interpreter = ctx.actions.declare_file("{}/bin/{}".format(venv, py_exe_basename))
560+
interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
560561
ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path))
561562

562563
elif runtime.interpreter:
@@ -568,7 +569,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
568569
# declare_symlink() is required to ensure that the resulting file
569570
# in runfiles is always a symlink. An RBE implementation, for example,
570571
# may choose to write what symlink() points to instead.
571-
interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename))
572+
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
572573

573574
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
574575
rel_path = relative_path(
@@ -581,7 +582,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
581582
ctx.actions.symlink(output = interpreter, target_path = rel_path)
582583
else:
583584
py_exe_basename = paths.basename(runtime.interpreter_path)
584-
interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename))
585+
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
585586
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
586587
interpreter_actual_path = runtime.interpreter_path
587588

@@ -618,89 +619,104 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
618619
},
619620
computed_substitutions = computed_subs,
620621
)
621-
site_packages_symlinks = _create_site_packages_symlinks(ctx, site_packages)
622+
623+
venv_dir_map = {
624+
VenvSymlinkKind.BIN: bin_dir,
625+
VenvSymlinkKind.LIB: site_packages,
626+
}
627+
venv_symlinks = _create_venv_symlinks(ctx, venv_dir_map)
622628

623629
return struct(
624630
interpreter = interpreter,
625631
recreate_venv_at_runtime = recreate_venv_at_runtime,
626632
# Runfiles root relative path or absolute path
627633
interpreter_actual_path = interpreter_actual_path,
628-
files_without_interpreter = [pyvenv_cfg, pth, site_init] + site_packages_symlinks,
634+
files_without_interpreter = [pyvenv_cfg, pth, site_init] + venv_symlinks,
629635
# string; venv-relative path to the site-packages directory.
630636
venv_site_packages = venv_site_packages,
631637
)
632638

633-
def _create_site_packages_symlinks(ctx, site_packages):
634-
"""Creates symlinks within site-packages.
639+
def _create_venv_symlinks(ctx, venv_dir_map):
640+
"""Creates symlinks within the venv.
635641
636642
Args:
637643
ctx: current rule ctx
638-
site_packages: runfiles-root-relative path to the site-packages directory
644+
venv_dir_map: mapping of VenvSymlinkKind constants to the
645+
venv path.
639646
640647
Returns:
641648
{type}`list[File]` list of the File symlink objects created.
642649
"""
643650

644-
# maps site-package symlink to the runfiles path it should point to
651+
# maps venv-relative path to the runfiles path it should point to
645652
entries = depset(
646653
# NOTE: Topological ordering is used so that dependencies closer to the
647654
# binary have precedence in creating their symlinks. This allows the
648655
# binary a modicum of control over the result.
649656
order = "topological",
650657
transitive = [
651-
dep[PyInfo].site_packages_symlinks
658+
dep[PyInfo].venv_symlinks
652659
for dep in ctx.attr.deps
653660
if PyInfo in dep
654661
],
655662
).to_list()
663+
656664
link_map = _build_link_map(entries)
665+
venv_files = []
666+
for kind, kind_map in link_map.items():
667+
base = venv_dir_map[kind]
668+
for venv_path, link_to in kind_map.items():
669+
venv_link = ctx.actions.declare_symlink(paths.join(base, venv_path))
670+
venv_link_rf_path = runfiles_root_path(ctx, venv_link.short_path)
671+
rel_path = relative_path(
672+
# dirname is necessary because a relative symlink is relative to
673+
# the directory the symlink resides within.
674+
from_ = paths.dirname(venv_link_rf_path),
675+
to = link_to,
676+
)
677+
ctx.actions.symlink(output = venv_link, target_path = rel_path)
678+
venv_files.append(venv_link)
657679

658-
sp_files = []
659-
for sp_dir_path, link_to in link_map.items():
660-
sp_link = ctx.actions.declare_symlink(paths.join(site_packages, sp_dir_path))
661-
sp_link_rf_path = runfiles_root_path(ctx, sp_link.short_path)
662-
rel_path = relative_path(
663-
# dirname is necessary because a relative symlink is relative to
664-
# the directory the symlink resides within.
665-
from_ = paths.dirname(sp_link_rf_path),
666-
to = link_to,
667-
)
668-
ctx.actions.symlink(output = sp_link, target_path = rel_path)
669-
sp_files.append(sp_link)
670-
return sp_files
680+
return venv_files
671681

672682
def _build_link_map(entries):
683+
# dict[str kind, dict[str rel_path, str link_to_path]]
673684
link_map = {}
674-
for link_to_runfiles_path, site_packages_path in entries:
675-
if site_packages_path in link_map:
685+
for entry in entries:
686+
kind = entry.kind
687+
kind_map = link_map.setdefault(kind, {})
688+
if entry.venv_path in kind_map:
676689
# We ignore duplicates by design. The dependency closer to the
677690
# binary gets precedence due to the topological ordering.
678691
continue
679692
else:
680-
link_map[site_packages_path] = link_to_runfiles_path
693+
kind_map[entry.venv_path] = entry.link_to_path
681694

682695
# An empty link_to value means to not create the site package symlink.
683696
# Because of the topological ordering, this allows binaries to remove
684697
# entries by having an earlier dependency produce empty link_to values.
685-
for sp_dir_path, link_to in link_map.items():
686-
if not link_to:
687-
link_map.pop(sp_dir_path)
698+
for kind, kind_map in link_map.items():
699+
for dir_path, link_to in kind_map.items():
700+
if not link_to:
701+
kind_map.pop(dir_path)
688702

689-
# Remove entries that would be a child path of a created symlink.
690-
# Earlier entries have precedence to match how exact matches are handled.
703+
# dict[str kind, dict[str rel_path, str link_to_path]]
691704
keep_link_map = {}
692-
for _ in range(len(link_map)):
693-
if not link_map:
694-
break
695-
dirname, value = link_map.popitem()
696-
keep_link_map[dirname] = value
697-
698-
prefix = dirname + "/" # Add slash to prevent /X matching /XY
699-
for maybe_suffix in link_map.keys():
700-
maybe_suffix += "/" # Add slash to prevent /X matching /XY
701-
if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix):
702-
link_map.pop(maybe_suffix)
703705

706+
# Remove entries that would be a child path of a created symlink.
707+
# Earlier entries have precedence to match how exact matches are handled.
708+
for kind, kind_map in link_map.items():
709+
keep_kind_map = keep_link_map.setdefault(kind, {})
710+
for _ in range(len(kind_map)):
711+
if not kind_map:
712+
break
713+
dirname, value = kind_map.popitem()
714+
keep_kind_map[dirname] = value
715+
prefix = dirname + "/" # Add slash to prevent /X matching /XY
716+
for maybe_suffix in kind_map.keys():
717+
maybe_suffix += "/" # Add slash to prevent /X matching /XY
718+
if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix):
719+
kind_map.pop(maybe_suffix)
704720
return keep_link_map
705721

706722
def _map_each_identity(v):

0 commit comments

Comments
 (0)