Skip to content

Commit 0196825

Browse files
authored
feat: allow populating binary's venv site-packages with symlinks (#2617)
This implements functionality to allow libraries to populate the site-packages directory of downstream binaries. The basic implementation is: * Libraries provide tuples of `(runfile path, site packages path)` in the `PyInfo.site_packages_symlinks` field. * Binaries create symlinks (using declare_symlink) in their site-packages directory pointing to the runfiles paths libraries provide. The design was chosen because of the following properties: * The site-packages directory is relocatable * Populating site packages is cheap ( `O(number 3p dependencies)` ) * Dependencies are only created once in the runfiles, no matter how many how many binaries there that use them. This minimizes disk usage, file counts, inodes, etc. The `site_packages_symlinks` field is a depset with topological ordering. Using topological ordering allows dependencies closer to the binary to have precedence, which gives some basic control over what entries are used. Additionally, the runfiles path to link to can be None/empty, in which case, the directory in site-packages won't be created. This allows binaries to prevent creation of directories that might e.g. conflict. For now, this functionality is disabled by default. The flag `--venvs_site_packages=yes` can be set to allow using it, which is automatically enable it for pypi generated targets. When enabled, it does basic detection of implicit namespace directories, which allows multiple distributions to "install" into the the same site-packages directory. Though this functionality is primarily useful for dependencies from pypi (e.g. via pip.parse), it is not yet activated for those targets, for two main reasons: 1. The wheel extraction code creates pkgutil-style `__init__.py` shims during the repo-phase. The build phase can't distinguish these artifical rules_python generated shims from actual `__init__.py` files, which breaks the implicit namespace detection logic. 2. A flag guard is needed before changing the behavior. Even though how 3p libraries are added to sys.path is an implementation detail, the behavior has been there for many years, so an escape hatch should be added. Work towards #2156
1 parent 2bc3577 commit 0196825

File tree

34 files changed

+574
-33
lines changed

34 files changed

+574
-33
lines changed

.bazelrc

+2-2
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

+4
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ Unreleased changes template.
107107
please check the {obj}`uv.configure` tag class.
108108
* Add support for riscv64 linux platform.
109109
* (toolchains) Add python 3.13.2 and 3.12.9 toolchains
110+
* (providers) (experimental) {obj}`PyInfo.site_packages_symlinks` field added to
111+
allow specifying links to create within the venv site packages (only
112+
applicable with {obj}`--bootstrap_impl=script`)
113+
([#2156](https://github.com/bazelbuild/rules_python/issues/2156)).
110114

111115
{#v0-0-0-removed}
112116
### Removed

MODULE.bazel

+6
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",

docs/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ sphinx_stardocs(
8787
name = "bzl_api_docs",
8888
srcs = [
8989
"//python:defs_bzl",
90+
"//python:features_bzl",
9091
"//python:packaging_bzl",
9192
"//python:pip_bzl",
9293
"//python:py_binary_bzl",

docs/_includes/experimental_api.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:::{warning}
2+
3+
**Experimental API.** This API is still under development and may change or be
4+
removed without notice.
5+
:::

docs/api/rules_python/python/config_settings/index.md

+17
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,23 @@ Values:
213213
::::
214214

215215

216+
::::
217+
218+
:::{flag} venvs_site_packages
219+
220+
Determines if libraries use a site-packages layout for their files.
221+
222+
Note this flag only affects PyPI dependencies of `--bootstrap_impl=script` binaries
223+
224+
:::{include} /_includes/experimental_api.md
225+
:::
226+
227+
228+
Values:
229+
* `no` (default): Make libraries importable by adding to `sys.path`
230+
* `yes`: Make libraries importable by creating paths in a binary's site-packages directory.
231+
::::
232+
216233
::::{bzl:flag} bootstrap_impl
217234
Determine how programs implement their startup process.
218235

internal_dev_deps.bzl

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Dependencies that are needed for development and testing of rules_python itself."""
1616

1717
load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive", _http_file = "http_file")
18+
load("@bazel_tools//tools/build_defs/repo:local.bzl", "local_repository")
1819
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
1920
load("//python/private:internal_config_repo.bzl", "internal_config_repo") # buildifier: disable=bzl-visibility
2021

@@ -42,6 +43,11 @@ def rules_python_internal_deps():
4243
"""
4344
internal_config_repo(name = "rules_python_internal")
4445

46+
local_repository(
47+
name = "other",
48+
path = "tests/modules/other",
49+
)
50+
4551
http_archive(
4652
name = "bazel_skylib",
4753
sha256 = "bc283cdfcd526a52c3201279cda4bc298652efa898b10b4db0837dc51652756f",

python/BUILD.bazel

+3
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ bzl_library(
7979
bzl_library(
8080
name = "features_bzl",
8181
srcs = ["features.bzl"],
82+
deps = [
83+
"@rules_python_internal//:rules_python_config_bzl",
84+
],
8285
)
8386

8487
bzl_library(

python/config_settings/BUILD.bazel

+8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ load(
99
"LibcFlag",
1010
"PrecompileFlag",
1111
"PrecompileSourceRetentionFlag",
12+
"VenvsSitePackages",
1213
"VenvsUseDeclareSymlinkFlag",
1314
)
1415
load(
@@ -195,6 +196,13 @@ string_flag(
195196
visibility = ["//visibility:public"],
196197
)
197198

199+
string_flag(
200+
name = "venvs_site_packages",
201+
build_setting_default = VenvsSitePackages.NO,
202+
# NOTE: Only public because it is used in pip hub repos.
203+
visibility = ["//visibility:public"],
204+
)
205+
198206
define_pypi_internal_flags(
199207
name = "define_pypi_internal_flags",
200208
)

python/features.bzl

+42-1
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,49 @@ load("@rules_python_internal//:rules_python_config.bzl", "config")
1919
# See https://git-scm.com/docs/git-archive/2.29.0#Documentation/git-archive.txt-export-subst
2020
_VERSION_PRIVATE = "$Format:%(describe:tags=true)$"
2121

22+
def _features_typedef():
23+
"""Information about features rules_python has implemented.
24+
25+
::::{field} precompile
26+
:type: bool
27+
28+
True if the precompile attributes are available.
29+
30+
:::{versionadded} 0.33.0
31+
:::
32+
::::
33+
34+
::::{field} py_info_site_packages_symlinks
35+
36+
True if the `PyInfo.site_packages_symlinks` field is available.
37+
38+
:::{versionadded} VERSION_NEXT_FEATURE
39+
:::
40+
::::
41+
42+
::::{field} uses_builtin_rules
43+
:type: bool
44+
45+
True if the rules are using the Bazel-builtin implementation.
46+
47+
:::{versionadded} 1.1.0
48+
:::
49+
::::
50+
51+
::::{field} version
52+
:type: str
53+
54+
The rules_python version. This is a semver format, e.g. `X.Y.Z` with
55+
optional trailing `-rcN`. For unreleased versions, it is an empty string.
56+
:::{versionadded} 0.38.0
57+
::::
58+
"""
59+
2260
features = struct(
23-
version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "",
61+
TYPEDEF = _features_typedef,
62+
# keep sorted
2463
precompile = True,
64+
py_info_site_packages_symlinks = True,
2565
uses_builtin_rules = not config.enable_pystar,
66+
version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "",
2667
)

python/private/attributes.bzl

+11
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,17 @@ These are typically `py_library` rules.
254254
255255
Targets that only provide data files used at runtime belong in the `data`
256256
attribute.
257+
258+
:::{note}
259+
The order of this list can matter because it affects the order that information
260+
from dependencies is merged in, which can be relevant depending on the ordering
261+
mode of depsets that are merged.
262+
263+
* {obj}`PyInfo.site_packages_symlinks` uses topological ordering.
264+
265+
See {obj}`PyInfo` for more information about the ordering of its depsets and
266+
how its fields are merged.
267+
:::
257268
""",
258269
),
259270
"precompile": lambda: attrb.String(

python/private/builders.bzl

+10-3
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

+16-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None
3030
# Extensions without the dot
3131
_PYTHON_SOURCE_EXTENSIONS = ["py"]
3232

33+
# Extensions that mean a file is relevant to Python
34+
PYTHON_FILE_EXTENSIONS = [
35+
"dll", # Python C modules, Windows specific
36+
"dylib", # Python C modules, Mac specific
37+
"py",
38+
"pyc",
39+
"pyi",
40+
"so", # Python C modules, usually Linux
41+
]
42+
3343
def create_binary_semantics_struct(
3444
*,
3545
create_executable,
@@ -367,7 +377,8 @@ def create_py_info(
367377
required_pyc_files,
368378
implicit_pyc_files,
369379
implicit_pyc_source_files,
370-
imports):
380+
imports,
381+
site_packages_symlinks = []):
371382
"""Create PyInfo provider.
372383
373384
Args:
@@ -385,13 +396,17 @@ def create_py_info(
385396
implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files
386397
that a binary can choose to include.
387398
imports: depset of strings; the import path values to propagate.
399+
site_packages_symlinks: {type}`list[tuple[str, str]]` tuples of
400+
`(runfiles_path, site_packages_path)` for symlinks to create
401+
in the consuming binary's venv site packages.
388402
389403
Returns:
390404
A tuple of the PyInfo instance and a depset of the
391405
transitive sources collected from dependencies (the latter is only
392406
necessary for deprecated extra actions support).
393407
"""
394408
py_info = PyInfoBuilder()
409+
py_info.site_packages_symlinks.add(site_packages_symlinks)
395410
py_info.direct_original_sources.add(original_sources)
396411
py_info.direct_pyc_files.add(required_pyc_files)
397412
py_info.direct_pyi_files.add(ctx.files.pyi_srcs)

python/private/enum.bzl

+20
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,23 @@ def enum(methods = {}, **kwargs):
4343

4444
self = struct(__members__ = members, **kwargs)
4545
return self
46+
47+
def _FlagEnum_flag_values(self):
48+
return sorted(self.__members__.values())
49+
50+
def FlagEnum(**kwargs):
51+
"""Define an enum specialized for flags.
52+
53+
Args:
54+
**kwargs: members of the enum.
55+
56+
Returns:
57+
{type}`FlagEnum` struct. This is an enum with the following extras:
58+
* `flag_values`: A function that returns a sorted list of the
59+
flag values (enum `__members__`). Useful for passing to the
60+
`values` attribute for string flags.
61+
"""
62+
return enum(
63+
methods = dict(flag_values = _FlagEnum_flag_values),
64+
**kwargs
65+
)

python/private/flags.bzl

+17-21
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,7 @@ unnecessary files when all that are needed are flag definitions.
1919
"""
2020

2121
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
22-
load(":enum.bzl", "enum")
23-
24-
def _FlagEnum_flag_values(self):
25-
return sorted(self.__members__.values())
26-
27-
def FlagEnum(**kwargs):
28-
"""Define an enum specialized for flags.
29-
30-
Args:
31-
**kwargs: members of the enum.
32-
33-
Returns:
34-
{type}`FlagEnum` struct. This is an enum with the following extras:
35-
* `flag_values`: A function that returns a sorted list of the
36-
flag values (enum `__members__`). Useful for passing to the
37-
`values` attribute for string flags.
38-
"""
39-
return enum(
40-
methods = dict(flag_values = _FlagEnum_flag_values),
41-
**kwargs
42-
)
22+
load(":enum.bzl", "FlagEnum", "enum")
4323

4424
def _AddSrcsToRunfilesFlag_is_enabled(ctx):
4525
value = ctx.attr._add_srcs_to_runfiles_flag[BuildSettingInfo].value
@@ -138,6 +118,22 @@ VenvsUseDeclareSymlinkFlag = FlagEnum(
138118
get_value = _venvs_use_declare_symlink_flag_get_value,
139119
)
140120

121+
def _venvs_site_packages_is_enabled(ctx):
122+
if not ctx.attr.experimental_venvs_site_packages:
123+
return False
124+
flag_value = ctx.attr.experimental_venvs_site_packages[BuildSettingInfo].value
125+
return flag_value == VenvsSitePackages.YES
126+
127+
# Decides if libraries try to use a site-packages layout using site_packages_symlinks
128+
# buildifier: disable=name-conventions
129+
VenvsSitePackages = FlagEnum(
130+
# Use site_packages_symlinks
131+
YES = "yes",
132+
# Don't use site_packages_symlinks
133+
NO = "no",
134+
is_enabled = _venvs_site_packages_is_enabled,
135+
)
136+
141137
# Used for matching freethreaded toolchains and would have to be used in wheels
142138
# as well.
143139
# buildifier: disable=name-conventions

0 commit comments

Comments
 (0)