Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b4e6496
basic impl
rickeylev Apr 23, 2026
c676d6e
fix tests
rickeylev Apr 23, 2026
10ace3b
dont pass select to repo-generated init creation
rickeylev Apr 25, 2026
0fd7c6d
fix testing on workspace
rickeylev Apr 25, 2026
7d2893f
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Apr 25, 2026
16c5100
format, lint fix
rickeylev Apr 25, 2026
9e53c53
fix pip example
rickeylev Apr 25, 2026
76cc0e0
format
rickeylev Apr 25, 2026
9470aac
test: fix windows paths for whl_with_data test
rickeylev Apr 25, 2026
5b57de2
fix: prevent grouping of top-level bin, include, and data in venvs
rickeylev Apr 25, 2026
103ec9a
fix: add alias for is_venvs_site_packages and add whl_with_data to wo…
rickeylev Apr 25, 2026
f41bfda
fix: add WORKSPACE file and export all files in whl_with_data repo
rickeylev Apr 25, 2026
fe1ad5f
revert: rename of is_venvs_site_packages and whl_with_data workspace …
rickeylev Apr 25, 2026
b35951a
fix: add bazel_skylib to pip_parse_vendored example
rickeylev Apr 25, 2026
52dfac5
fix: add bazel_skylib to multi_python_versions and pip_parse examples
rickeylev Apr 25, 2026
45611a5
docs: add DATA field to VenvSymlinkKind docstring
rickeylev Apr 25, 2026
fd6cda6
fix: undefined data_arg in whl_library_targets.bzl
rickeylev Apr 25, 2026
958e61e
fix: robustness in venvs_site_packages_libs_test for Windows paths
rickeylev Apr 25, 2026
194693c
fix: make bin/s3cmd expectation conditional in pip_parse_test.py
rickeylev Apr 25, 2026
82b9acc
refactor: rename is_venvs_site_packages to _is_venvs_site_packages
rickeylev Apr 25, 2026
dd2b0b3
pass create_inits() non-selet value, add select value afterwards
rickeylev Apr 25, 2026
3ed727e
change test to enforce windows uses Scripts/Include in venv
rickeylev Apr 25, 2026
7561c72
make pip_parse_test.py work with venv_site_packages
rickeylev Apr 25, 2026
24a94a2
add second wheel with data to test merging/overlap. rename first
rickeylev Apr 25, 2026
1fabeea
format
rickeylev Apr 25, 2026
473966b
revert passing bzlmod/venv_site_packages to pip_parse_test. not neede…
rickeylev Apr 25, 2026
dd5d8c2
remove defunct comment
rickeylev Apr 26, 2026
08aeea4
(re)add _yes suffix to is_venv_site_packages config setting
rickeylev Apr 26, 2026
747f7cb
add basis for data overlap test
rickeylev Apr 26, 2026
03da47f
add overlap tests for include, bin; cleanup test
rickeylev Apr 26, 2026
de2a11f
format
rickeylev Apr 26, 2026
82e9e2a
make tests pass with workspace
rickeylev Apr 26, 2026
cdb2cf1
lint
rickeylev Apr 26, 2026
e7ca80d
tests: support whl_from_dir_repo on Windows
rickeylev Apr 26, 2026
e16757e
Merge branch 'main' into chore.win.whl.tests
rickeylev Apr 26, 2026
e35d1a4
move data scheme to venv root
rickeylev Apr 26, 2026
e38fd1c
move cannot_be_linked_directly init earlier
rickeylev Apr 26, 2026
9184bcc
re-add data prefix to pip_parse test. It is verifying the whl_library…
rickeylev Apr 26, 2026
db05136
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Apr 26, 2026
51f614b
handle when directly linking to a file on windows
rickeylev Apr 26, 2026
9681318
always incldue data label
rickeylev Apr 26, 2026
50cc8f0
update changelog
rickeylev Apr 26, 2026
cd18acc
cleanup
rickeylev Apr 26, 2026
8c3daaa
Merge branch 'chore.win.whl.tests' into whl.with.data
rickeylev Apr 26, 2026
835d0a9
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Apr 26, 2026
f08b096
feat(venv): make installed wheel scripts runnable
rickeylev Apr 26, 2026
ec6926e
feat: make wheel entry points runnable in venv
rickeylev Apr 27, 2026
181c4a5
skip runnable test on windows
rickeylev Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ END_UNRELEASED_TEMPLATE
### Changed
* (gazelle) WORKSPACE's bazel-gazelle dependency bumped from 0.36.0 to 0.47.0.
The go version was also bumped from 1.21.13 to 1.22.9.
* (pypi) The data files of a wheel (bin, includes, etc) are now always included
as a library's data dependencies.

{#v0-0-0-fixed}
### Fixed
Expand All @@ -74,6 +76,8 @@ END_UNRELEASED_TEMPLATE
adding `config_setting` labels to all registered toolchains.
* (windows) Full venv support for Windows is available. Set
{obj}`--venvs_site_packages=yes` to enable.
* (test/binaries) When {obj}`--venv_site_packages=yes` is enabled,
wheel `data`, `bin`, and `include` files are populated into the venv.
* (runfiles) Added a pathlib-compatible API: {obj}`Runfiles.root()`
Fixes [#3296](https://github.com/bazel-contrib/rules_python/issues/3296).
* (toolchains) `3.13.12`, `3.14.3` Python toolchain from [20260325] release.
Expand Down
2 changes: 2 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ use_repo(
"somepkg_with_build_files",
"whl_library_extras_direct_dep",
"whl_with_build_files",
"whl_with_data1",
"whl_with_data2",
)

dev_rules_python_config = use_extension(
Expand Down
20 changes: 10 additions & 10 deletions examples/pip_parse/pip_parse_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,16 @@ def test_data(self):
self.assertIsNotNone(actual)
actual = self._remove_leading_dirs(actual.split(" "))

self.assertListEqual(
actual,
[
"data/share/doc/packages/s3cmd/INSTALL.md",
"data/share/doc/packages/s3cmd/LICENSE",
"data/share/doc/packages/s3cmd/NEWS",
"data/share/doc/packages/s3cmd/README.md",
"data/share/man/man1/s3cmd.1",
],
)
expected = [
"bin/s3cmd",
"data/share/doc/packages/s3cmd/INSTALL.md",
"data/share/doc/packages/s3cmd/LICENSE",
"data/share/doc/packages/s3cmd/NEWS",
"data/share/doc/packages/s3cmd/README.md",
"data/share/man/man1/s3cmd.1",
]

self.assertListEqual(actual, expected)

def test_dist_info(self):
actual = os.environ.get("WHEEL_DIST_INFO_CONTENTS")
Expand Down
24 changes: 24 additions & 0 deletions internal_dev_setup.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
load("//python/private:pythons_hub.bzl", "hub_repo") # buildifier: disable=bzl-visibility
load("//python/private:runtime_env_repo.bzl", "runtime_env_repo") # buildifier: disable=bzl-visibility
load("//python/private/pypi:deps.bzl", "pypi_deps") # buildifier: disable=bzl-visibility
load("//python/private/pypi:whl_library.bzl", "whl_library") # buildifier: disable=bzl-visibility
load("//tests/support/whl_from_dir:whl_from_dir_repo.bzl", "whl_from_dir_repo") # buildifier: disable=bzl-visibility

def rules_python_internal_setup():
"""Setup for development and testing of rules_python itself."""
Expand Down Expand Up @@ -59,3 +61,25 @@ def rules_python_internal_setup():
bazel_features_deps()
rules_shell_dependencies()
rules_shell_toolchains()

whl_from_dir_repo(
name = "whl_with_data1_whl",
root = "//tests/repos/whl_with_data1:BUILD.bazel",
output = "whl_with_data1-1.0-any-none-any.whl",
)
whl_library(
name = "whl_with_data1",
whl_file = "@whl_with_data1_whl//:whl_with_data1-1.0-any-none-any.whl",
requirement = "whl-with-data1",
)

whl_from_dir_repo(
name = "whl_with_data2_whl",
root = "//tests/repos/whl_with_data2:BUILD.bazel",
output = "whl_with_data2-1.0-any-none-any.whl",
)
whl_library(
name = "whl_with_data2",
whl_file = "@whl_with_data2_whl//:whl_with_data2-1.0-any-none-any.whl",
requirement = "whl-with-data2",
)
2 changes: 1 addition & 1 deletion python/config_settings/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ string_flag(
)

config_setting(
name = "is_venvs_site_packages",
name = "_is_venvs_site_packages_yes",
flag_values = {
":venvs_site_packages": VenvsSitePackages.YES,
},
Expand Down
8 changes: 8 additions & 0 deletions python/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,14 @@ bzl_library(
],
)

alias(
name = "venv_bin_rewriter",
actual = select({
"@platforms//os:windows": ":venv_bin_rewriter.ps1",
"//conditions:default": ":venv_bin_rewriter.sh",
}),
)

bzl_library(
name = "venv_runfiles_bzl",
srcs = ["venv_runfiles.bzl"],
Expand Down
22 changes: 22 additions & 0 deletions python/private/internal_dev_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ def _internal_dev_deps_impl(mctx):
enable_implicit_namespace_pkgs = False,
)

whl_from_dir_repo(
name = "whl_with_data1_whl",
root = "//tests/repos/whl_with_data1:BUILD.bazel",
output = "whl_with_data1-1.0-any-none-any.whl",
)
whl_library(
name = "whl_with_data1",
whl_file = "@whl_with_data1_whl//:whl_with_data1-1.0-any-none-any.whl",
requirement = "whl-with-data1",
)

whl_from_dir_repo(
name = "whl_with_data2_whl",
root = "//tests/repos/whl_with_data2:BUILD.bazel",
output = "whl_with_data2-1.0-any-none-any.whl",
)
whl_library(
name = "whl_with_data2",
whl_file = "@whl_with_data2_whl//:whl_with_data2-1.0-any-none-any.whl",
requirement = "whl-with-data2",
)

_whl_library_from_dir(
name = "whl_library_extras_direct_dep",
root = "//tests/pypi/whl_library/testdata/pkg:BUILD.bazel",
Expand Down
30 changes: 22 additions & 8 deletions python/private/py_executable.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ accepting arbitrary Python versions.
default = "//python/private:uncachable_version_file",
allow_files = True,
),
"_venv_bin_rewriter": lambda: attrb.Label(
default = "//python/private:venv_bin_rewriter",
allow_files = True,
cfg = "exec",
),
"_venvs_use_declare_symlink_flag": lambda: attrb.Label(
default = labels.VENVS_USE_DECLARE_SYMLINK,
providers = [BuildSettingInfo],
Expand Down Expand Up @@ -575,9 +580,13 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
computed_substitutions = computed_subs,
)

# See https://docs.python.org/3/library/sysconfig.html#posix-prefix
# for how schemes map under the venv.
venv_dir_map = {
VenvSymlinkKind.BIN: venv_details.bin_dir,
VenvSymlinkKind.BIN: "{}/{}".format(venv_ctx_rel_root, venv_details.bin_dir),
VenvSymlinkKind.LIB: site_packages,
VenvSymlinkKind.INCLUDE: "{}/{}".format(venv_ctx_rel_root, venv_details.include_dir),
VenvSymlinkKind.DATA: venv_ctx_rel_root,
}
venv_app_files = create_venv_app_files(
ctx,
Expand Down Expand Up @@ -659,7 +668,7 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa

recreate_venv_at_runtime = False

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

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

rel_path = relative_path(
Expand All @@ -690,7 +699,7 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
)
ctx.actions.symlink(output = interpreter, target_path = rel_path)
else:
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename))
interpreter_runfiles.add(interpreter)
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
else:
Expand All @@ -715,7 +724,8 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
interpreter = interpreter,
pyvenv_cfg = pyvenv_cfg,
site_packages = site_packages,
bin_dir = bin_dir,
bin_dir = "bin",
include_dir = "include",
recreate_venv_at_runtime = recreate_venv_at_runtime,
interpreter_runfiles = interpreter_runfiles.build(ctx),
interpreter_symlinks = depset(),
Expand Down Expand Up @@ -777,7 +787,8 @@ def _create_venv_windows(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_
interpreter = interpreter,
pyvenv_cfg = None,
site_packages = site_packages,
bin_dir = venv_bin_ctx_rel_path,
bin_dir = venv_bin_rel_path,
include_dir = "Include",
recreate_venv_at_runtime = True,
interpreter_runfiles = interpreter_runfiles.build(ctx),
interpreter_symlinks = interpreter_symlinks.build(),
Expand All @@ -789,6 +800,7 @@ def _venv_details(
pyvenv_cfg,
site_packages,
bin_dir,
include_dir,
recreate_venv_at_runtime,
interpreter_runfiles,
interpreter_symlinks):
Expand All @@ -801,8 +813,10 @@ def _venv_details(
pyvenv_cfg = pyvenv_cfg,
# str; venv-relative path to the site-packages directory
site_packages = site_packages,
# str; ctx-relative path to the venv's bin directory.
# str; venv-relative path to the venv's bin directory.
bin_dir = bin_dir,
# str; venv-relative-path to the venv's include directory.
include_dir = include_dir,
# bool; True if the venv needs to be recreated at runtime (because the
# build-time construction isn't sufficient). False if the build-time
# constructed venv is sufficient.
Expand Down
7 changes: 7 additions & 0 deletions python/private/py_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def _VenvSymlinkKind_typedef():

Indicates to create paths under the venv's include directory.
:::

:::{field} DATA
:type: object

Indicates to create paths under the venv's data directory.
:::
"""

# buildifier: disable=name-conventions
Expand All @@ -45,6 +51,7 @@ VenvSymlinkKind = struct(
BIN = "BIN",
LIB = "LIB",
INCLUDE = "INCLUDE",
DATA = "DATA",
)

def _VenvSymlinkEntry_init(**kwargs):
Expand Down
2 changes: 1 addition & 1 deletion python/private/pypi/whl_installer/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def unzip(self, directory: str) -> None:
destination = installer.destinations.SchemeDictionaryDestination(
installation_schemes,
# TODO Should entry_point scripts also be handled by installer rather than custom code?
interpreter="/dev/null",
interpreter="python",
script_kind="posix",
Comment thread
rickeylev marked this conversation as resolved.
destdir=directory,
bytecode_optimization_levels=[],
Expand Down
10 changes: 8 additions & 2 deletions python/private/pypi/whl_library_targets.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ _BAZEL_REPO_FILE_GLOBS = [
"WORKSPACE.bazel",
]

_IS_VENV_SITE_PACKAGES_YES = Label("//python/config_settings:_is_venvs_site_packages_yes")

def whl_library_targets_from_requires(
*,
name,
Expand Down Expand Up @@ -195,7 +197,7 @@ def whl_library_targets(
include = ["site-packages/*.dist-info/**"],
),
DATA_LABEL: dict(
include = ["data/**"],
include = ["data/**", "bin/**", "include/**"],
),
}

Expand Down Expand Up @@ -369,7 +371,7 @@ def whl_library_targets(

if not enable_implicit_namespace_pkgs:
generated_namespace_package_files = select({
Label("//python/config_settings:is_venvs_site_packages"): [],
_IS_VENV_SITE_PACKAGES_YES: [],
"//conditions:default": rules.create_inits(
srcs = srcs + data + pyi_srcs,
ignored_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so.
Expand All @@ -379,6 +381,10 @@ def whl_library_targets(
namespace_package_files += generated_namespace_package_files
srcs = srcs + generated_namespace_package_files

# This is done after create_inits() is called so that the data scheme
# files don't have such files created in their directories.
data = data + [DATA_LABEL]

rules.py_library(
name = py_library_label,
srcs = srcs,
Expand Down
28 changes: 28 additions & 0 deletions python/private/venv_bin_rewriter.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[CmdletBinding()]
param(
[Parameter(Position=0, Mandatory=$true)]
[string]$InFile,

[Parameter(Position=1, Mandatory=$true)]
[string]$OutFile
)

if ($InFile.EndsWith(".exe") -or $InFile.EndsWith(".dll")) {
Copy-Item -Path $InFile -Destination $OutFile
exit 0
}

$firstLine = Get-Content -Path $InFile -TotalCount 1 -ErrorAction SilentlyContinue

if ($firstLine -match "^#!python") {
$content = Get-Content -Path $InFile | Select-Object -Skip 1
$wrapper = @'
#!/bin/sh
'''exec' "$(dirname "$0")/python3" "$0" "$@"
' '''
Comment thread
rickeylev marked this conversation as resolved.
'@
Set-Content -Path $OutFile -Value $wrapper -Encoding UTF8
Add-Content -Path $OutFile -Value $content -Encoding UTF8
} else {
Copy-Item -Path $InFile -Destination $OutFile
}
19 changes: 19 additions & 0 deletions python/private/venv_bin_rewriter.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/sh
set -eu

IN="$1"
OUT="$2"

if head -n 1 "$IN" | grep -q "^#!python"; then
echo "#!/bin/sh" > "$OUT"
# Polyglot re-exec gibberish.
# Shell treats first line's quotes as a quoted command to execute. It then
# re-execs itself with Python, which treats the triple quoted strings
# as plain strings and ignores them.
echo "'''exec' \"\$(dirname \"\$0\")/python3\" \"\$0\" \"\$@\"" >> "$OUT"
Comment thread
rickeylev marked this conversation as resolved.
echo "' '''" >> "$OUT"
tail -n +2 "$IN" >> "$OUT"
else
cp "$IN" "$OUT"
fi
chmod +x "$OUT"
Loading
Loading