Skip to content

Commit 01be3e2

Browse files
committed
wip: libs in venv site packages
1 parent 34e82cd commit 01be3e2

File tree

23 files changed

+369
-9
lines changed

23 files changed

+369
-9
lines changed

.bazelrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
55
# To update these lines, execute
66
# `bazel run @rules_bazel_integration_test//tools:update_deleted_packages`
7-
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
8-
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
7+
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma
8+
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma
99

1010
test --test_output=errors
1111

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,14 @@ Unreleased changes template.
6060

6161
{#v0-0-0-added}
6262
### Added
63-
* Nothing added.
63+
* (providers) {obj}`PyInfo.site_packages_symlinks` field added to allow
64+
specifying links to create within the venv site packages
65+
(only applicable with {obj}`--bootstrap_impl=script`)
66+
([#2156](https://github.com/bazelbuild/rules_python/issues/2156)).
67+
* (rules) {obj}`py_library.site_packages_root` attribute added to allow
68+
specifying a library's sources follow a site-packages file layout.
69+
(only applicable with {obj}`--bootstrap_impl=script`)
70+
([#2156](https://github.com/bazelbuild/rules_python/issues/2156)).
6471

6572
{#v0-0-0-removed}
6673
### Removed

MODULE.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ bazel_dep(name = "rules_shell", version = "0.3.0", dev_dependency = True)
8585
bazel_dep(name = "rules_multirun", version = "0.9.0", dev_dependency = True)
8686
bazel_dep(name = "bazel_ci_rules", version = "1.0.0", dev_dependency = True)
8787
bazel_dep(name = "rules_pkg", version = "1.0.1", dev_dependency = True)
88+
bazel_dep(name = "other", version = "0", dev_dependency = True)
8889

8990
# Extra gazelle plugin deps so that WORKSPACE.bzlmod can continue including it for e2e tests.
9091
# We use `WORKSPACE.bzlmod` because it is impossible to have dev-only local overrides.
@@ -106,6 +107,11 @@ local_path_override(
106107
path = "gazelle",
107108
)
108109

110+
local_path_override(
111+
module_name = "other",
112+
path = "tests/modules/other",
113+
)
114+
109115
dev_python = use_extension(
110116
"//python/extensions:python.bzl",
111117
"python",

python/features.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ features = struct(
2323
version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "",
2424
precompile = True,
2525
uses_builtin_rules = not config.enable_pystar,
26+
site_packages_root_attr = True,
2627
)

python/private/attributes.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ that depend on this rule. The strings are repo-runfiles-root relative,
282282
283283
Absolute paths (paths that start with `/`) and paths that references a path
284284
above the execution root are not allowed and will result in an error.
285+
286+
:::{attention}
287+
Setting both this and the {attr}`site_packages_root` attribute may result in
288+
undefined behavior. Both will result in the code being importable, but from
289+
different sys.path (and thus `__file__`) entries.
290+
:::
285291
""",
286292
),
287293
}

python/private/builders.bzl

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@
1515

1616
load("@bazel_skylib//lib:types.bzl", "types")
1717

18-
def _DepsetBuilder():
19-
"""Create a builder for a depset."""
18+
def _DepsetBuilder(order = None):
19+
"""Create a builder for a depset.
20+
21+
Args:
22+
order: {type}`str | None` The order to initialize the depset to, if any.
23+
24+
Returns:
25+
{type}`DepsetBuilder`
26+
"""
2027

2128
# buildifier: disable=uninitialized
2229
self = struct(
23-
_order = [None],
30+
_order = [order],
2431
add = lambda *a, **k: _DepsetBuilder_add(self, *a, **k),
2532
build = lambda *a, **k: _DepsetBuilder_build(self, *a, **k),
2633
direct = [],

python/private/common.bzl

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None
3030
# Extensions without the dot
3131
_PYTHON_SOURCE_EXTENSIONS = ["py"]
3232

33+
# Extensions that make a file considered importable
34+
PYTHON_FILE_EXTENSIONS = [
35+
"py",
36+
"so", # Python C modules, usually Linux
37+
"dylib", # Python C modules, Mac specific
38+
"pyc",
39+
"dll", # Python C modules, Windows specific
40+
]
41+
3342
def create_binary_semantics_struct(
3443
*,
3544
create_executable,
@@ -413,7 +422,8 @@ def create_py_info(
413422
required_pyc_files,
414423
implicit_pyc_files,
415424
implicit_pyc_source_files,
416-
imports):
425+
imports,
426+
site_packages_symlinks = []):
417427
"""Create PyInfo provider.
418428
419429
Args:
@@ -431,13 +441,17 @@ def create_py_info(
431441
implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files
432442
that a binary can choose to include.
433443
imports: depset of strings; the import path values to propagate.
444+
site_packages_symlinks: {type}`list[tuple[str, str]]` tuples of
445+
`(runfiles_path, site_packages_path)` for symlinks to create
446+
in the consuming binary's venv site packages.
434447
435448
Returns:
436449
A tuple of the PyInfo instance and a depset of the
437450
transitive sources collected from dependencies (the latter is only
438451
necessary for deprecated extra actions support).
439452
"""
440453
py_info = PyInfoBuilder()
454+
py_info.site_packages_symlinks.add(site_packages_symlinks)
441455
py_info.direct_original_sources.add(original_sources)
442456
py_info.direct_pyc_files.add(required_pyc_files)
443457
py_info.direct_pyi_files.add(ctx.files.pyi_srcs)

python/private/py_executable.bzl

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,15 +591,78 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
591591
},
592592
computed_substitutions = computed_subs,
593593
)
594+
site_packages_symlinks = _create_site_packages_symlinks(ctx, site_packages)
594595

595596
return struct(
596597
interpreter = interpreter,
597598
recreate_venv_at_runtime = not venvs_use_declare_symlink_enabled,
598599
# Runfiles root relative path or absolute path
599600
interpreter_actual_path = interpreter_actual_path,
600-
files_without_interpreter = [pyvenv_cfg, pth, site_init],
601+
files_without_interpreter = [pyvenv_cfg, pth, site_init] + site_packages_symlinks,
601602
)
602603

604+
def _create_site_packages_symlinks(ctx, site_packages):
605+
"""Creates symlinks within site-packages.
606+
607+
Args:
608+
ctx: current rule ctx
609+
site_packages: runfiles-root-relative path to the site-packages directory
610+
611+
Returns:
612+
{type}`list[File]` list of the File symlink objects created.
613+
"""
614+
615+
# maps site-package symlink to the runfiles path it should point to
616+
entries = depset(
617+
# NOTE: Topological ordering is used so that dependencies closer to the
618+
# binary have precedence in creating their symlinks. This allows the
619+
# binary a modicum of control over the result.
620+
order = "topological",
621+
transitive = [
622+
dep[PyInfo].site_packages_symlinks
623+
for dep in ctx.attr.deps
624+
if PyInfo in dep
625+
],
626+
).to_list()
627+
link_map = {}
628+
for link_to_runfiles_path, site_packages_path in entries:
629+
if site_packages_path in link_map:
630+
# We ignore duplicates by design. The dependency closer to the
631+
# binary gets precedence due to the topological ordering.
632+
continue
633+
else:
634+
link_map[site_packages_path] = link_to_runfiles_path
635+
636+
# An empty link_to value means to not create the site package symlink.
637+
# Because of the topological ordering, this allows binaries to remove
638+
# entries by having an earlier dependency produce empty link_to values
639+
for sp_dir_path, link_to in link_map.items():
640+
if not link_to:
641+
link_map.pop(sp_dir_path)
642+
643+
# This is N^2; we can certainly do better by sorting and exploiting the
644+
# order.
645+
# A trailing slash is appended / to prevent /X matching /XY
646+
sp_dirs = [x + "/" for x in link_map.keys()]
647+
for search_for in sp_dirs:
648+
for prefix in sp_dirs:
649+
if search_for != prefix and search_for.startswith(prefix):
650+
fail("sub-link: {} under {}", search_for, prefix)
651+
652+
sp_files = []
653+
for sp_dir_path, link_to in link_map.items():
654+
sp_link = ctx.actions.declare_symlink(paths.join(site_packages, sp_dir_path))
655+
sp_link_rf_path = runfiles_root_path(ctx, sp_link.short_path)
656+
rel_path = relative_path(
657+
# dirname is necessary because a relative symlink is relative to
658+
# the directory the symlink resides within.
659+
from_ = paths.dirname(sp_link_rf_path),
660+
to = link_to,
661+
)
662+
ctx.actions.symlink(output = sp_link, target_path = rel_path)
663+
sp_files.append(sp_link)
664+
return sp_files
665+
603666
def _map_each_identity(v):
604667
return v
605668

python/private/py_info.bzl

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ def _PyInfo_init(
4242
direct_original_sources = depset(),
4343
transitive_original_sources = depset(),
4444
direct_pyi_files = depset(),
45-
transitive_pyi_files = depset()):
45+
transitive_pyi_files = depset(),
46+
site_packages_symlinks = depset()):
4647
_check_arg_type("transitive_sources", "depset", transitive_sources)
4748

4849
# Verify it's postorder compatible, but retain is original ordering.
@@ -70,6 +71,7 @@ def _PyInfo_init(
7071
"has_py2_only_sources": has_py2_only_sources,
7172
"has_py3_only_sources": has_py2_only_sources,
7273
"imports": imports,
74+
"site_packages_symlinks": site_packages_symlinks,
7375
"transitive_implicit_pyc_files": transitive_implicit_pyc_files,
7476
"transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files,
7577
"transitive_original_sources": transitive_original_sources,
@@ -140,6 +142,31 @@ A depset of import path strings to be added to the `PYTHONPATH` of executable
140142
Python targets. These are accumulated from the transitive `deps`.
141143
The order of the depset is not guaranteed and may be changed in the future. It
142144
is recommended to use `default` order (the default).
145+
""",
146+
"site_packages_symlinks": """
147+
:type: depset[tuple[str | None, str]]
148+
149+
A depset with `topological` ordering.
150+
151+
Tuples of `(runfiles_path, site_packages_path)`. Where
152+
* `runfiles_path` is a runfiles-root relative path. It is the path that
153+
has the code to make importable. If `None` or empty string, then it means
154+
to not create a site packages directory with the `site_packages_path`
155+
name.
156+
* `site_packages_path` is a path relative to the site-packages directory of
157+
the venv for whatever creates the venv (typically py_binary). It makes
158+
the code in `runfiles_path` available for import. Note that this
159+
is created as a "raw" symlink (via `declare_symlink`).
160+
161+
:::{tip}
162+
The topological ordering means dependencies earlier and closer to the consumer
163+
have precedence. This allows e.g. a binary to add dependencies that override
164+
values from further way dependencies, such as forcing symlinks to point to
165+
specific paths or preventing symlinks from being created.
166+
:::
167+
168+
:::{versionadded} VERSION_NEXT_FEATURE
169+
:::
143170
""",
144171
"transitive_implicit_pyc_files": """
145172
:type: depset[File]
@@ -266,6 +293,7 @@ def PyInfoBuilder():
266293
transitive_pyc_files = builders.DepsetBuilder(),
267294
transitive_pyi_files = builders.DepsetBuilder(),
268295
transitive_sources = builders.DepsetBuilder(),
296+
site_packages_symlinks = builders.DepsetBuilder(order = "topological"),
269297
)
270298
return self
271299

@@ -351,6 +379,7 @@ def _PyInfoBuilder_merge_all(self, transitive, *, direct = []):
351379
self.transitive_original_sources.add(info.transitive_original_sources)
352380
self.transitive_pyc_files.add(info.transitive_pyc_files)
353381
self.transitive_pyi_files.add(info.transitive_pyi_files)
382+
self.site_packages_symlinks.add(info.site_packages_symlinks)
354383

355384
return self
356385

@@ -400,6 +429,7 @@ def _PyInfoBuilder_build(self):
400429
transitive_original_sources = self.transitive_original_sources.build(),
401430
transitive_pyc_files = self.transitive_pyc_files.build(),
402431
transitive_pyi_files = self.transitive_pyi_files.build(),
432+
site_packages_symlinks = self.site_packages_symlinks.build(),
403433
)
404434
else:
405435
kwargs = {}

0 commit comments

Comments
 (0)