Skip to content

Commit 547521e

Browse files
authored
feat(windows): site-packages venv support (bazel-contrib#3718)
This implements Windows support for library files being populated in the venv site-packages directory (i.e. `--venv_site_packages=yes`). This is mostly just an expansion of the work done to make venvs work on Windows. It mostly just adds the site-packages paths to the list of (venv_path, symlinks) values that is currently used for the interpreter files. The main notable change is that creation of the site-packages directory also requires traversing the whole site-packages directory and re-creating its structure in the temporary venv. This isn't ideal, but there isn't much other option. Work towards bazel-contrib#3245
1 parent 19a2c04 commit 547521e

23 files changed

Lines changed: 674 additions & 350 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ END_UNRELEASED_TEMPLATE
6767

6868
{#v0-0-0-added}
6969
### Added
70+
* (windows) Full venv support for Windows is available. Set
71+
{obj}`--venvs_site_packages=yes` to enable.
7072
* (runfiles) Added a pathlib-compatible API: {obj}`Runfiles.root()`
7173
Fixes [#3296](https://github.com/bazel-contrib/rules_python/issues/3296).
7274
* (toolchains) `3.13.12`, `3.14.3` Python toolchain from [20260325] release.

docs/conf.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,6 @@
9595
"pypi-dependencies.html": "pypi/index.html",
9696
}
9797

98-
# Adapted from the template code:
99-
# https://github.com/readthedocs/readthedocs.org/blob/main/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl
100-
if os.environ.get("READTHEDOCS") == "True":
101-
# Must come first because it can interfere with other extensions, according
102-
# to the original conf.py template comments
103-
extensions.insert(0, "readthedocs_ext.readthedocs")
104-
10598
exclude_patterns = ["_includes/*"]
10699
templates_path = ["_templates"]
107100
primary_domain = None # The default is 'py', which we don't make much use of

docs/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ dependencies = [
1616
"pefile",
1717
"pyelftools",
1818
"macholib",
19+
"markupsafe",
1920
]

docs/requirements.txt

Lines changed: 270 additions & 173 deletions
Large diffs are not rendered by default.

python/private/attributes.bzl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,14 @@ AGNOSTIC_TEST_ATTRS = _init_agnostic_test_attrs()
542542
# but still accept Python source-agnostic settings.
543543
AGNOSTIC_BINARY_ATTRS = dicts.add(AGNOSTIC_EXECUTABLE_ATTRS)
544544

545+
WINDOWS_CONSTRAINTS_ATTRS = {
546+
"_windows_constraints": lambda: attrb.LabelList(
547+
default = [
548+
"@platforms//os:windows",
549+
],
550+
),
551+
}
552+
545553
# Attribute names common to all Python rules
546554
COMMON_ATTR_NAMES = [
547555
"compatible_with",

python/private/common.bzl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,31 @@ BUILTIN_BUILD_PYTHON_ZIP = [] if config.bazel_10_or_later else [
4949
"//command_line_option:build_python_zip",
5050
]
5151

52+
# Not an actual provider. Provider used for memory efficiency.
53+
# buildifier: disable=name-conventions
54+
ExplicitSymlink = provider(
55+
doc = """
56+
A runfile that should be created as a symlink pointing to a specific location.
57+
58+
This is only needed on Windows, where Bazel doesn't preserve declare_symlink
59+
with relative paths. This is basically manually captures what using
60+
declare_symlink(), symlink() and runfiles like so would capture:
61+
62+
```
63+
link = declare_symlink(...)
64+
link_to_path = relative_path(from=link, to=target)
65+
symlink(output=link, target_path=link_to_path)
66+
runfiles.add([link, target])
67+
```
68+
""",
69+
fields = {
70+
"files": "depset[File] of files that should be included if this symlink is used",
71+
"link_to_path": "Path the symlink should point to",
72+
"runfiles_path": "runfiles-root-relative path for the symlink",
73+
"venv_path": "venv-root-relative path for the symlink",
74+
},
75+
)
76+
5277
def maybe_builtin_build_python_zip(value, settings = None):
5378
settings = settings or {}
5479
if not config.bazel_10_or_later:
@@ -460,6 +485,17 @@ def target_platform_has_any_constraint(ctx, constraints):
460485
return True
461486
return False
462487

488+
def is_windows_platform(ctx):
489+
"""Check if target platform is windows.
490+
491+
Args:
492+
ctx: rule context.
493+
494+
Returns:
495+
True if target platform is windows.
496+
"""
497+
return target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints)
498+
463499
def relative_path(from_, to):
464500
"""Compute a relative path from one path to another.
465501

python/private/py_executable.bzl

Lines changed: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ load(
3030
"PrecompileAttr",
3131
"PycCollectionAttr",
3232
"REQUIRED_EXEC_GROUP_BUILDERS",
33+
"WINDOWS_CONSTRAINTS_ATTRS",
3334
"apply_config_settings_attr",
3435
)
3536
load(":builders.bzl", "builders")
3637
load(":cc_helper.bzl", "cc_helper")
3738
load(
3839
":common.bzl",
40+
"ExplicitSymlink",
3941
"collect_cc_info",
4042
"collect_deps",
4143
"collect_imports",
@@ -49,10 +51,10 @@ load(
4951
"csv",
5052
"filter_to_py_srcs",
5153
"is_bool",
54+
"is_windows_platform",
5255
"maybe_create_repo_mapping",
5356
"relative_path",
5457
"runfiles_root_path",
55-
"target_platform_has_any_constraint",
5658
)
5759
load(":common_labels.bzl", "labels")
5860
load(":flags.bzl", "BootstrapImplFlag", "VenvsUseDeclareSymlinkFlag", "read_possibly_native_flag")
@@ -73,37 +75,14 @@ _EXTERNAL_PATH_PREFIX = "external"
7375
_ZIP_RUNFILES_DIRECTORY_NAME = "runfiles"
7476
_INIT_PY = "__init__.py"
7577

76-
# buildifier: disable=name-conventions
77-
ExplicitSymlink = provider(
78-
doc = """
79-
A runfile that should be created as a symlink pointing to a specific location.
80-
81-
This is only needed on Windows, where Bazel doesn't preserve declare_symlink
82-
with relative paths. This is basically manually captures what using
83-
declare_symlink(), symlink() and runfiles like so would capture:
84-
85-
```
86-
link = declare_symlink(...)
87-
link_to_path = relative_path(from=link, to=target)
88-
symlink(output=link, target_path=link_to_path)
89-
runfiles.add([link, target])
90-
```
91-
""",
92-
fields = {
93-
"files": "depset[File] of files that should be included if this symlink is used",
94-
"link_to_path": "Path the symlink should point to",
95-
"runfiles_path": "runfiles-root-relative path for the symlink",
96-
"venv_path": "venv-root-relative path for the symlink",
97-
},
98-
)
99-
10078
# Non-Google-specific attributes for executables
10179
# These attributes are for rules that accept Python sources.
10280
EXECUTABLE_ATTRS = dicts.add(
10381
COMMON_ATTRS,
10482
AGNOSTIC_EXECUTABLE_ATTRS,
10583
PY_SRCS_ATTRS,
10684
IMPORTS_ATTRS,
85+
WINDOWS_CONSTRAINTS_ATTRS,
10786
# starlark flags attributes
10887
{
10988
"_build_python_zip_flag": attr.label(default = "//python/config_settings:build_python_zip"),
@@ -257,11 +236,6 @@ accepting arbitrary Python versions.
257236
default = labels.VENVS_USE_DECLARE_SYMLINK,
258237
providers = [BuildSettingInfo],
259238
),
260-
"_windows_constraints": lambda: attrb.LabelList(
261-
default = [
262-
"@platforms//os:windows",
263-
],
264-
),
265239
"_zipper": lambda: attrb.Label(
266240
cfg = "exec",
267241
executable = True,
@@ -323,7 +297,7 @@ def _create_executable(
323297
extra_deps):
324298
_ = is_test, cc_details, native_deps_details # @unused
325299

326-
is_windows = target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints)
300+
is_windows = is_windows_platform(ctx)
327301

328302
if is_windows:
329303
if not executable.extension == "exe":
@@ -447,14 +421,14 @@ WARNING: Target: {}
447421
use_zip_file = build_zip_enabled,
448422
python_binary_path = runtime_details.executable_interpreter_path,
449423
)
450-
if not build_zip_enabled:
451-
# On Windows, the main executable has an "exe" extension, so
452-
# here we re-use the un-extensioned name for the bootstrap output.
453-
bootstrap_output = ctx.actions.declare_file(base_executable_name)
454424

455-
# The launcher looks for the non-zip executable next to
456-
# itself, so add it to the default outputs.
457-
extra_default_outputs.append(bootstrap_output)
425+
# On Windows, the main executable has an "exe" extension, so
426+
# here we re-use the un-extensioned name for the bootstrap output.
427+
bootstrap_output = ctx.actions.declare_file(base_executable_name)
428+
429+
# The launcher looks for the non-zip executable next to
430+
# itself, so add it to the default outputs.
431+
extra_default_outputs.append(bootstrap_output)
458432

459433
if should_create_executable_zip:
460434
if bootstrap_output != None:
@@ -516,6 +490,9 @@ WARNING: Target: {}
516490
# runfiles; runfiles for the app itself (e.g its deps, but no Python
517491
# runtime files)
518492
app_runfiles = app_runfiles.build(ctx),
493+
# depset[ExplicitSymlink]None; symlinks that should be created in
494+
# the venv to augment app_runfiles
495+
venv_app_symlinks = venv.lib_symlinks if venv else None,
519496
# File|None; the venv `bin/python3` file, if any.
520497
venv_python_exe = venv.interpreter if venv else None,
521498
# runfiles|None; runfiles in the venv for the interpreter
@@ -561,7 +538,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
561538
else:
562539
interpreter_actual_path = runtime.interpreter_path
563540

564-
is_windows = target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints)
541+
is_windows = is_windows_platform(ctx)
565542
if is_windows:
566543
venv_details = _create_venv_windows(
567544
ctx,
@@ -644,6 +621,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
644621
lib_runfiles = ctx.runfiles(
645622
root_symlinks = venv_app_files.runfiles_symlinks,
646623
),
624+
lib_symlinks = venv_app_files.explicit_symlinks,
647625
)
648626

649627
def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_path):
@@ -937,9 +915,12 @@ def _create_stage1_bootstrap(
937915
}
938916
computed_subs = ctx.actions.template_dict()
939917
if venv:
918+
runtime_venv_symlinks = depset(
919+
transitive = [venv.interpreter_symlinks, venv.lib_symlinks],
920+
)
940921
computed_subs.add_joined(
941922
"%runtime_venv_symlinks%",
942-
venv.interpreter_symlinks,
923+
runtime_venv_symlinks,
943924
join_with = "\n",
944925
map_each = _map_runtime_venv_symlink,
945926
)
@@ -1250,8 +1231,6 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
12501231
)
12511232
))
12521233

1253-
app_runfiles = exec_result.app_runfiles
1254-
12551234
providers = []
12561235

12571236
_add_provider_default_info(
@@ -1265,13 +1244,14 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
12651244
_add_provider_run_environment_info(providers, ctx, inherited_environment)
12661245
_add_provider_py_executable_info(
12671246
providers,
1268-
app_runfiles = app_runfiles,
1247+
app_runfiles = exec_result.app_runfiles,
12691248
build_data_file = runfiles_details.build_data_file,
12701249
interpreter_args = ctx.attr.interpreter_args,
12711250
interpreter_path = runtime_details.executable_interpreter_path,
12721251
main_py = main_py,
12731252
runfiles_without_exe = runfiles_details.runfiles_without_exe,
12741253
stage2_bootstrap = exec_result.stage2_bootstrap,
1254+
venv_app_symlinks = exec_result.venv_app_symlinks,
12751255
venv_interpreter_runfiles = exec_result.venv_interpreter_runfiles,
12761256
venv_interpreter_symlinks = exec_result.venv_interpreter_symlinks,
12771257
venv_python_exe = exec_result.venv_python_exe,
@@ -1313,7 +1293,7 @@ def _validate_executable(ctx):
13131293
).format(ctx.attr.main, ctx.attr.main_module))
13141294

13151295
def _declare_executable_file(ctx):
1316-
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
1296+
if is_windows_platform(ctx):
13171297
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
13181298
else:
13191299
executable = ctx.actions.declare_file(ctx.label.name)
@@ -1882,6 +1862,7 @@ def _add_provider_py_executable_info(
18821862
main_py,
18831863
runfiles_without_exe,
18841864
stage2_bootstrap,
1865+
venv_app_symlinks,
18851866
venv_interpreter_runfiles,
18861867
venv_interpreter_symlinks,
18871868
venv_python_exe):
@@ -1896,6 +1877,8 @@ def _add_provider_py_executable_info(
18961877
main_py: File; the main .py entry point.
18971878
runfiles_without_exe: runfiles; the default runfiles, but without the executable.
18981879
stage2_bootstrap: File; the stage 2 bootstrap script.
1880+
venv_app_symlinks: depset[ExplicitSymlink]; symlinks to create for the
1881+
venv that are the application (deps, etc).
18991882
venv_interpreter_runfiles: runfiles; runfiles specific to the interpreter for the venv.
19001883
venv_interpreter_symlinks: depset[ExplicitSymlink]; interpreter-specific symlinks to create for the venv.
19011884
venv_python_exe: File; the python executable in the venv.
@@ -1908,6 +1891,7 @@ def _add_provider_py_executable_info(
19081891
main = main_py,
19091892
runfiles_without_exe = runfiles_without_exe,
19101893
stage2_bootstrap = stage2_bootstrap,
1894+
venv_app_symlinks = venv_app_symlinks,
19111895
venv_interpreter_runfiles = venv_interpreter_runfiles,
19121896
venv_interpreter_symlinks = venv_interpreter_symlinks,
19131897
venv_python_exe = venv_python_exe,

python/private/py_executable_info.bzl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@ implementation isn't being used.
7777
7878
:::{versionadded} 1.9.0
7979
:::
80+
""",
81+
"venv_app_symlinks": """
82+
:type: depset[ExplicitSymlink] | None
83+
84+
Symlinks that are specific to the application within the venv (e.g.
85+
dependencies).
86+
87+
Only used with Windows for files that would have used `declare_symlink()`
88+
to create relative symlinks. These may overlap with paths in runfiles; it's
89+
up to the consumer to determine how to handle such overlaps.
90+
91+
:::{versionadded} VERSION_NEXT_FEATURE
92+
:::
8093
""",
8194
"venv_interpreter_runfiles": """
8295
:type: runfiles | None

0 commit comments

Comments
 (0)