Skip to content

Commit 12eac29

Browse files
authored
feat(venv): support data, include, and scripts schemes (#3726)
Currently, the files from the data, headers, and scripts portions of a wheel don't end up in the proper sub-directories of the venv. This means the full files of a distribution aren't available at the typical location in the venv, making it harder to integrate with standard tools. To fix, simply map the directories to paths in the venv and give them similar treatment as site-packages. The spec says certain first-level directories of the `.data` directory map to specific scheme paths, which `whl_library` already handles. Here's a listing of the `wheel data directory -> install scheme path key -> whl_library directory` relationships * purelib -> purelib -> site-packages * platlib -> platlib -> site-packages * headers -> include -> include * scripts -> scripts -> bin * data -> data -> data Relevant reading: * Packaging specification: https://packaging.python.org/en/latest/specifications/binary-distribution-format * Posix install scheme paths: https://docs.python.org/3/library/sysconfig.html#posix-prefix * Windows install scheme paths: https://docs.python.org/3/library/sysconfig.html#nt The whl_library rule uses posix names for extracting. When materialized into a binary's venv, platform specific names are used: * bin -> (posix) bin; (Windows) Scripts * include -> (posix) include; (Windows) Include (capital i) * data -> venv root directory Along the way ... * The data files (files under the "data" scheme of a whl) are now always included as part of depending on the library. They would be in included in venv_site_packages=yes mode, so this better aligns behavior of the two modes. * Rename `is_venv_site_packages` to `_is_venv_site_packages_yes` to better represent the purpose and visibility of it. * Make whl_from_dir support Windows. Testing of venvs relies on using it for testing various special cases.
1 parent 82ae73e commit 12eac29

51 files changed

Lines changed: 318 additions & 53 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ END_UNRELEASED_TEMPLATE
6060
### Changed
6161
* (gazelle) WORKSPACE's bazel-gazelle dependency bumped from 0.36.0 to 0.47.0.
6262
The go version was also bumped from 1.21.13 to 1.22.9.
63+
* (pypi) The data files of a wheel (bin, includes, etc) are now always included
64+
as a library's data dependencies.
6365

6466
{#v0-0-0-fixed}
6567
### Fixed
@@ -74,6 +76,8 @@ END_UNRELEASED_TEMPLATE
7476
adding `config_setting` labels to all registered toolchains.
7577
* (windows) Full venv support for Windows is available. Set
7678
{obj}`--venvs_site_packages=yes` to enable.
79+
* (test/binaries) When {obj}`--venv_site_packages=yes` is enabled,
80+
wheel `data`, `bin`, and `include` files are populated into the venv.
7781
* (runfiles) Added a pathlib-compatible API: {obj}`Runfiles.root()`
7882
Fixes [#3296](https://github.com/bazel-contrib/rules_python/issues/3296).
7983
* (toolchains) `3.13.12`, `3.14.3` Python toolchain from [20260325] release.

MODULE.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ use_repo(
246246
"somepkg_with_build_files",
247247
"whl_library_extras_direct_dep",
248248
"whl_with_build_files",
249+
"whl_with_data1",
250+
"whl_with_data2",
249251
)
250252

251253
dev_rules_python_config = use_extension(

examples/pip_parse/pip_parse_test.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,16 @@ def test_data(self):
5252
self.assertIsNotNone(actual)
5353
actual = self._remove_leading_dirs(actual.split(" "))
5454

55-
self.assertListEqual(
56-
actual,
57-
[
58-
"data/share/doc/packages/s3cmd/INSTALL.md",
59-
"data/share/doc/packages/s3cmd/LICENSE",
60-
"data/share/doc/packages/s3cmd/NEWS",
61-
"data/share/doc/packages/s3cmd/README.md",
62-
"data/share/man/man1/s3cmd.1",
63-
],
64-
)
55+
expected = [
56+
"bin/s3cmd",
57+
"data/share/doc/packages/s3cmd/INSTALL.md",
58+
"data/share/doc/packages/s3cmd/LICENSE",
59+
"data/share/doc/packages/s3cmd/NEWS",
60+
"data/share/doc/packages/s3cmd/README.md",
61+
"data/share/man/man1/s3cmd.1",
62+
]
63+
64+
self.assertListEqual(actual, expected)
6565

6666
def test_dist_info(self):
6767
actual = os.environ.get("WHEEL_DIST_INFO_CONTENTS")

internal_dev_setup.bzl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
2626
load("//python/private:pythons_hub.bzl", "hub_repo") # buildifier: disable=bzl-visibility
2727
load("//python/private:runtime_env_repo.bzl", "runtime_env_repo") # buildifier: disable=bzl-visibility
2828
load("//python/private/pypi:deps.bzl", "pypi_deps") # buildifier: disable=bzl-visibility
29+
load("//python/private/pypi:whl_library.bzl", "whl_library") # buildifier: disable=bzl-visibility
30+
load("//tests/support/whl_from_dir:whl_from_dir_repo.bzl", "whl_from_dir_repo") # buildifier: disable=bzl-visibility
2931

3032
def rules_python_internal_setup():
3133
"""Setup for development and testing of rules_python itself."""
@@ -59,3 +61,25 @@ def rules_python_internal_setup():
5961
bazel_features_deps()
6062
rules_shell_dependencies()
6163
rules_shell_toolchains()
64+
65+
whl_from_dir_repo(
66+
name = "whl_with_data1_whl",
67+
root = "//tests/repos/whl_with_data1:BUILD.bazel",
68+
output = "whl_with_data1-1.0-any-none-any.whl",
69+
)
70+
whl_library(
71+
name = "whl_with_data1",
72+
whl_file = "@whl_with_data1_whl//:whl_with_data1-1.0-any-none-any.whl",
73+
requirement = "whl-with-data1",
74+
)
75+
76+
whl_from_dir_repo(
77+
name = "whl_with_data2_whl",
78+
root = "//tests/repos/whl_with_data2:BUILD.bazel",
79+
output = "whl_with_data2-1.0-any-none-any.whl",
80+
)
81+
whl_library(
82+
name = "whl_with_data2",
83+
whl_file = "@whl_with_data2_whl//:whl_with_data2-1.0-any-none-any.whl",
84+
requirement = "whl-with-data2",
85+
)

python/config_settings/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ string_flag(
229229
)
230230

231231
config_setting(
232-
name = "is_venvs_site_packages",
232+
name = "_is_venvs_site_packages_yes",
233233
flag_values = {
234234
":venvs_site_packages": VenvsSitePackages.YES,
235235
},

python/private/internal_dev_deps.bzl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ def _internal_dev_deps_impl(mctx):
9191
enable_implicit_namespace_pkgs = False,
9292
)
9393

94+
whl_from_dir_repo(
95+
name = "whl_with_data1_whl",
96+
root = "//tests/repos/whl_with_data1:BUILD.bazel",
97+
output = "whl_with_data1-1.0-any-none-any.whl",
98+
)
99+
whl_library(
100+
name = "whl_with_data1",
101+
whl_file = "@whl_with_data1_whl//:whl_with_data1-1.0-any-none-any.whl",
102+
requirement = "whl-with-data1",
103+
)
104+
105+
whl_from_dir_repo(
106+
name = "whl_with_data2_whl",
107+
root = "//tests/repos/whl_with_data2:BUILD.bazel",
108+
output = "whl_with_data2-1.0-any-none-any.whl",
109+
)
110+
whl_library(
111+
name = "whl_with_data2",
112+
whl_file = "@whl_with_data2_whl//:whl_with_data2-1.0-any-none-any.whl",
113+
requirement = "whl-with-data2",
114+
)
115+
94116
_whl_library_from_dir(
95117
name = "whl_library_extras_direct_dep",
96118
root = "//tests/pypi/whl_library/testdata/pkg:BUILD.bazel",

python/private/py_executable.bzl

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,13 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
575575
computed_substitutions = computed_subs,
576576
)
577577

578+
# See https://docs.python.org/3/library/sysconfig.html#posix-prefix
579+
# for how schemes map under the venv.
578580
venv_dir_map = {
579-
VenvSymlinkKind.BIN: venv_details.bin_dir,
581+
VenvSymlinkKind.BIN: "{}/{}".format(venv_ctx_rel_root, venv_details.bin_dir),
580582
VenvSymlinkKind.LIB: site_packages,
583+
VenvSymlinkKind.INCLUDE: "{}/{}".format(venv_ctx_rel_root, venv_details.include_dir),
584+
VenvSymlinkKind.DATA: venv_ctx_rel_root,
581585
}
582586
venv_app_files = create_venv_app_files(
583587
ctx,
@@ -659,7 +663,7 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
659663

660664
recreate_venv_at_runtime = False
661665

662-
bin_dir = "{}/bin".format(venv_ctx_rel_root)
666+
venv_bin_ctx_rel_path = "{}/bin".format(venv_ctx_rel_root)
663667
if create_full_venv:
664668
# Some wrappers around the interpreter (e.g. pyenv) use the program
665669
# name to decide what to do, so preserve the name.
@@ -671,15 +675,15 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
671675
# When the venv symlinks are disabled, the $venv/bin/python3 file isn't
672676
# needed or used at runtime. However, the zip code uses the interpreter
673677
# File object to figure out some paths.
674-
interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
678+
interpreter = ctx.actions.declare_file("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename))
675679
ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path))
676680

677681
elif runtime.interpreter:
678682
# Even though ctx.actions.symlink() is used, using
679683
# declare_symlink() is required to ensure that the resulting file
680684
# in runfiles is always a symlink. An RBE implementation, for example,
681685
# may choose to write what symlink() points to instead.
682-
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
686+
interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename))
683687
interpreter_runfiles.add(interpreter)
684688

685689
rel_path = relative_path(
@@ -690,7 +694,7 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
690694
)
691695
ctx.actions.symlink(output = interpreter, target_path = rel_path)
692696
else:
693-
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
697+
interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename))
694698
interpreter_runfiles.add(interpreter)
695699
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
696700
else:
@@ -715,7 +719,8 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
715719
interpreter = interpreter,
716720
pyvenv_cfg = pyvenv_cfg,
717721
site_packages = site_packages,
718-
bin_dir = bin_dir,
722+
bin_dir = "bin",
723+
include_dir = "include",
719724
recreate_venv_at_runtime = recreate_venv_at_runtime,
720725
interpreter_runfiles = interpreter_runfiles.build(ctx),
721726
interpreter_symlinks = depset(),
@@ -777,7 +782,8 @@ def _create_venv_windows(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_
777782
interpreter = interpreter,
778783
pyvenv_cfg = None,
779784
site_packages = site_packages,
780-
bin_dir = venv_bin_ctx_rel_path,
785+
bin_dir = venv_bin_rel_path,
786+
include_dir = "Include",
781787
recreate_venv_at_runtime = True,
782788
interpreter_runfiles = interpreter_runfiles.build(ctx),
783789
interpreter_symlinks = interpreter_symlinks.build(),
@@ -789,6 +795,7 @@ def _venv_details(
789795
pyvenv_cfg,
790796
site_packages,
791797
bin_dir,
798+
include_dir,
792799
recreate_venv_at_runtime,
793800
interpreter_runfiles,
794801
interpreter_symlinks):
@@ -801,8 +808,10 @@ def _venv_details(
801808
pyvenv_cfg = pyvenv_cfg,
802809
# str; venv-relative path to the site-packages directory
803810
site_packages = site_packages,
804-
# str; ctx-relative path to the venv's bin directory.
811+
# str; venv-relative path to the venv's bin directory.
805812
bin_dir = bin_dir,
813+
# str; venv-relative-path to the venv's include directory.
814+
include_dir = include_dir,
806815
# bool; True if the venv needs to be recreated at runtime (because the
807816
# build-time construction isn't sufficient). False if the build-time
808817
# constructed venv is sufficient.

python/private/py_info.bzl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def _VenvSymlinkKind_typedef():
3737
3838
Indicates to create paths under the venv's include directory.
3939
:::
40+
41+
:::{field} DATA
42+
:type: object
43+
44+
Indicates to create paths under the venv's data directory.
45+
:::
4046
"""
4147

4248
# buildifier: disable=name-conventions
@@ -45,6 +51,7 @@ VenvSymlinkKind = struct(
4551
BIN = "BIN",
4652
LIB = "LIB",
4753
INCLUDE = "INCLUDE",
54+
DATA = "DATA",
4855
)
4956

5057
def _VenvSymlinkEntry_init(**kwargs):

python/private/pypi/whl_library_targets.bzl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ _BAZEL_REPO_FILE_GLOBS = [
4242
"WORKSPACE.bazel",
4343
]
4444

45+
_IS_VENV_SITE_PACKAGES_YES = Label("//python/config_settings:_is_venvs_site_packages_yes")
46+
4547
def whl_library_targets_from_requires(
4648
*,
4749
name,
@@ -191,7 +193,7 @@ def whl_library_targets(
191193
include = ["site-packages/*.dist-info/**"],
192194
),
193195
DATA_LABEL: dict(
194-
include = ["data/**"],
196+
include = ["data/**", "bin/**", "include/**"],
195197
),
196198
}
197199

@@ -351,7 +353,7 @@ def whl_library_targets(
351353

352354
if not enable_implicit_namespace_pkgs:
353355
generated_namespace_package_files = select({
354-
Label("//python/config_settings:is_venvs_site_packages"): [],
356+
_IS_VENV_SITE_PACKAGES_YES: [],
355357
"//conditions:default": rules.create_inits(
356358
srcs = srcs + data + pyi_srcs,
357359
ignored_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so.
@@ -361,6 +363,10 @@ def whl_library_targets(
361363
namespace_package_files += generated_namespace_package_files
362364
srcs = srcs + generated_namespace_package_files
363365

366+
# This is done after create_inits() is called so that the data scheme
367+
# files don't have such files created in their directories.
368+
data = data + [DATA_LABEL]
369+
364370
rules.py_library(
365371
name = py_library_label,
366372
srcs = srcs,

0 commit comments

Comments
 (0)