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

34 files changed

Lines changed: 574 additions & 33 deletions

File tree

.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: 4 additions & 0 deletions
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

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

docs/BUILD.bazel

Lines changed: 1 addition & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 42 additions & 1 deletion
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
)

0 commit comments

Comments
 (0)