Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 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
827baf2
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Apr 28, 2026
5894daf
Merge branch 'refs/heads/whl.with.data' into venv.bin.runnable
rickeylev Apr 28, 2026
36b8dbd
checkpoint entry point template windows bat file
rickeylev Apr 28, 2026
85ea206
py_venv_entry_point et al rule for whl_library
rickeylev Apr 28, 2026
cfefcfb
format
rickeylev Apr 28, 2026
61bef05
bazel 7 fix
rickeylev Apr 28, 2026
47d4aae
fix: address CI failures on Windows and cross-compilation
rickeylev Apr 29, 2026
6a3dda0
fix: avoid target collisions and ensure scripts are movable on Bazel 7
rickeylev Apr 29, 2026
8012fbb
fix: ensure scripts have execute permissions in whl_extract
rickeylev Apr 29, 2026
0c9a831
fix: define os_name in whl_extract
rickeylev Apr 29, 2026
ec2cac0
fix: use root-relative labels in whl_library_targets.bzl
rickeylev Apr 29, 2026
a7c586c
fix: ensure destination directories exist in whl_extract
rickeylev Apr 29, 2026
2271e18
refactor(pypi): factor out permission fixing logic in whl_extract
rickeylev Apr 29, 2026
02be9e0
refactor(pypi): factor out permission fixing logic in whl_extract
rickeylev Apr 29, 2026
e276160
cleanup wheel.py, rn newlines
rickeylev Apr 29, 2026
da40401
fix cm name, normalize group name, remove defunct comment
rickeylev Apr 29, 2026
ad1202a
ignore scripts that have entry points, change parse_entry_points to r…
rickeylev Apr 29, 2026
8f97ae3
clean up how entry points are passed and parsed
rickeylev Apr 29, 2026
a5eda25
better dupe detection
rickeylev Apr 29, 2026
62a50c9
fix: address CI failures, deduplicate entry points, and improve cross…
rickeylev Apr 29, 2026
b9eb87e
format
rickeylev Apr 29, 2026
89c1a5a
switch to set for pip_parse_test
rickeylev Apr 29, 2026
cf5ab40
fix pip_parse example test: rewritten bins werent added to data label
rickeylev Apr 29, 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
8 changes: 4 additions & 4 deletions examples/pip_parse/pip_parse_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@ def test_entry_point(self):
def test_data(self):
actual = os.environ.get("WHEEL_DATA_CONTENTS")
self.assertIsNotNone(actual)
actual = self._remove_leading_dirs(actual.split(" "))
actual = set(self._remove_leading_dirs(actual.split(" ")))

expected = [
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)
self.assertEqual(actual, expected)

def test_dist_info(self):
actual = os.environ.get("WHEEL_DIST_INFO_CONTENTS")
Expand Down
30 changes: 30 additions & 0 deletions python/private/pypi/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ exports_files(
visibility = ["//visibility:public"],
)

alias(
name = "venv_entry_point_template",
actual = select({
"@platforms//os:windows": "venv_entry_point_template.bat",
"//conditions:default": "venv_entry_point_template.sh",
}),
visibility = ["//visibility:public"],
)

alias(
name = "venv_shebang_rewriter",
actual = select({
"@platforms//os:windows": "venv_shebang_rewriter.ps1",
"//conditions:default": "venv_shebang_rewriter.sh",
}),
visibility = ["//visibility:public"],
)

exports_files(
srcs = ["deps.bzl"],
visibility = ["//tools/private/update_deps:__pkg__"],
Expand Down Expand Up @@ -519,3 +537,15 @@ bzl_library(
name = "whl_target_platforms_bzl",
srcs = ["whl_target_platforms.bzl"],
)

bzl_library(
name = "venv_entry_point_bzl",
srcs = ["venv_entry_point.bzl"],
visibility = ["//visibility:public"],
)

bzl_library(
name = "venv_rewrite_shebang_bzl",
srcs = ["venv_rewrite_shebang.bzl"],
visibility = ["//visibility:public"],
)
1 change: 1 addition & 0 deletions python/private/pypi/generate_whl_library_build_bazel.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ _RENDER = {
"data_exclude": render.list,
"dependencies": render.list,
"dependencies_by_platform": lambda x: render.dict(x, value_repr = render.list),
"entry_points": render.dict_dict,
"extras": render.list,
"group_deps": render.list,
"include": str,
Expand Down
50 changes: 50 additions & 0 deletions python/private/pypi/venv_entry_point.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Rule for generating venv entry point scripts."""

load("//python/private:attributes.bzl", "WINDOWS_CONSTRAINTS_ATTRS")
load("//python/private:common.bzl", "is_windows_platform")
load("//python/private:rule_builders.bzl", "ruleb")

def _venv_entry_point_impl(ctx):
is_windows = is_windows_platform(ctx)

out_name = ctx.label.name
python_exe = ""
if is_windows:
out_name += ".bat"
python_exe = "pythonw.exe" if ctx.attr.group == "gui_scripts" else "python.exe"

out = ctx.actions.declare_file(out_name)

ctx.actions.expand_template(
template = ctx.file._template,
output = out,
substitutions = {
"{ATTRIBUTE}": ctx.attr.attribute,
"{MODULE}": ctx.attr.module,
"{PYTHON_EXE}": python_exe,
},
is_executable = True,
)

return [DefaultInfo(
files = depset([out]),
executable = out,
)]

_builder = ruleb.Rule(
implementation = _venv_entry_point_impl,
executable = True,
)
_builder.attrs.update({
"attribute": attr.string(mandatory = False, doc = "The attribute to call"),
"extras": attr.string(mandatory = False, doc = "The extras for the entry point"),
"group": attr.string(mandatory = False, doc = "The entry point group (e.g. console_scripts)"),
"module": attr.string(mandatory = True, doc = "The module to import"),
"_template": attr.label(
default = Label("//python/private/pypi:venv_entry_point_template"),
allow_single_file = True,
),
})
_builder.attrs.update(WINDOWS_CONSTRAINTS_ATTRS)

venv_entry_point = _builder.build()
8 changes: 8 additions & 0 deletions python/private/pypi/venv_entry_point_template.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@setlocal enabledelayedexpansion & "%~dp0{PYTHON_EXE}" -x "%~f0" %* & exit /b !ERRORLEVEL!
# -*- coding: utf-8 -*-
import re
import sys
from {MODULE} import {ATTRIBUTE}
if __name__ == "__main__":
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
sys.exit({ATTRIBUTE}())
10 changes: 10 additions & 0 deletions python/private/pypi/venv_entry_point_template.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this compatible with nix? I remember that /bin/bash should be replaced with /usr/bin/env bash on those setups, so I am wondering if it is similar for sh.

'''exec' "$(dirname "$0")/python3" "$0" "$@"
' '''
# -*- coding: utf-8 -*-
import re
import sys
from {MODULE} import {ATTRIBUTE}
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit({ATTRIBUTE}())
82 changes: 82 additions & 0 deletions python/private/pypi/venv_rewrite_shebang.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Rule for rewriting portable shebangs."""

load("//python/private:attributes.bzl", "WINDOWS_CONSTRAINTS_ATTRS")
load("//python/private:common.bzl", "is_windows_platform", "runfiles_root_path")
load("//python/private:py_info.bzl", "PyInfoBuilder", "VenvSymlinkEntry", "VenvSymlinkKind")
load("//python/private:rule_builders.bzl", "ruleb")

def _venv_rewrite_shebang_impl(ctx):
is_windows = is_windows_platform(ctx)

out_name = ctx.label.name
if is_windows:
out_name += ".bat"

out_file = ctx.actions.declare_file(out_name)
in_file = ctx.file.src

action_args = ctx.actions.args()
rewriter_file = ctx.files._venv_shebang_rewriter[0]
inputs = depset([in_file, rewriter_file])

if rewriter_file.path.endswith(".ps1"):
action_exe = "powershell.exe"
action_args.add_all([
"-ExecutionPolicy",
"Bypass",
"-NoProfile",
"-File",
rewriter_file,
])
else:
action_exe = ctx.attr._venv_shebang_rewriter[DefaultInfo].files_to_run

action_args.add(in_file)
action_args.add(out_file)
action_args.add("windows" if is_windows else "unix")

ctx.actions.run(
inputs = inputs,
outputs = [out_file],
executable = action_exe,
arguments = [action_args],
mnemonic = "PyVenvRewriteBin",
progress_message = "Rewriting venv bin script %{input}",
toolchain = None,
)

symlink = VenvSymlinkEntry(
kind = VenvSymlinkKind.BIN,
link_to_path = runfiles_root_path(ctx, out_file.short_path),
link_to_file = out_file,
venv_path = out_name,
package = ctx.attr.package,
version = ctx.attr.version,
files = depset([out_file]),
)
builder = PyInfoBuilder.new()
builder.venv_symlinks.add([symlink])
py_info = builder.build()

return [
DefaultInfo(files = depset([out_file]), executable = out_file),
py_info,
]

_builder = ruleb.Rule(
implementation = _venv_rewrite_shebang_impl,
executable = True,
)
_builder.attrs.update({
"package": attr.string(),
"src": attr.label(mandatory = True, allow_single_file = True),
"version": attr.string(),
"_venv_shebang_rewriter": attr.label(
default = "//python/private/pypi:venv_shebang_rewriter",
allow_files = True,
cfg = "exec",
),
})
_builder.attrs.update(WINDOWS_CONSTRAINTS_ATTRS)

venv_rewrite_shebang = _builder.build()
40 changes: 40 additions & 0 deletions python/private/pypi/venv_shebang_rewriter.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[CmdletBinding()]
param(
[Parameter(Position=0, Mandatory=$true)]
[string]$InFile,

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

[Parameter(Position=2, Mandatory=$true)]
[string]$TargetOs
)

$firstLine = Get-Content -Path $InFile -TotalCount 1 -ErrorAction SilentlyContinue
$content = Get-Content -Path $InFile | Select-Object -Skip 1

$Utf8NoBom = New-Object System.Text.UTF8Encoding $False

if ($TargetOs -eq "windows") {
if ($firstLine -match "^#!pythonw") {
$pythonExe = "pythonw.exe"
} else {
$pythonExe = "python.exe"
}
# A Batch-Python polyglot. Batch executes the first line and exits,
# while Python (via -x) ignores the first line and executes the rest.
$wrapper = "@setlocal enabledelayedexpansion & `"%~dp0$pythonExe`" -x `"%~f0`" %* & exit /b !ERRORLEVEL!"
[System.IO.File]::WriteAllText($OutFile, $wrapper + "`r`n", $Utf8NoBom)
} else {
# A Shell-Python polyglot. The shell executes the triple-quoted 'exec'
# command, re-running the script with python3 from the scripts directory.
# Python ignores the triple-quoted string and continues.
$wrapper = @"
#!/bin/sh
'''exec' "`$(dirname "`$0")/python3" "`$0" "`$@"
' '''
"@
[System.IO.File]::WriteAllText($OutFile, $wrapper + "`n", $Utf8NoBom)
}

[System.IO.File]::AppendAllLines($OutFile, $content, $Utf8NoBom)
27 changes: 27 additions & 0 deletions python/private/pypi/venv_shebang_rewriter.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/sh
set -eu

IN="$1"
OUT="$2"
TARGET_OS="$3"

FIRST_LINE=$(head -n 1 "$IN")

if [ "$TARGET_OS" = "windows" ]; then
case "$FIRST_LINE" in
"#!pythonw"*) PYTHON_EXE="pythonw.exe" ;;
*) PYTHON_EXE="python.exe" ;;
esac
# A Batch-Python polyglot. Batch executes the first line and exits,
# while Python (via -x) ignores the first line and executes the rest.
printf "@setlocal enabledelayedexpansion & \"%%~dp0$PYTHON_EXE\" -x \"%%~f0\" %%* & exit /b !ERRORLEVEL!\r\n" > "$OUT"
else
printf "#!/bin/sh\n" > "$OUT"
# A Shell-Python polyglot. The shell executes the triple-quoted 'exec'
# command, re-running the script with python3 from the scripts directory.
# Python ignores the triple-quoted string and continues.
printf "'''exec' \"\$(dirname \"\$0\")/python3\" \"\$0\" \"\$@\"\n' '''\n" >> "$OUT"
fi

tail -n +2 "$IN" >> "$OUT"
chmod +x "$OUT"
48 changes: 26 additions & 22 deletions python/private/pypi/whl_extract.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,8 @@ def whl_extract(rctx, *, whl_path, logger):
supports_whl_extraction = rp_config.supports_whl_extraction,
)

# Fix permissions on extracted files. Some wheels have files without read permissions set,
# which causes errors when trying to read them later.
os_name = repo_utils.get_platforms_os_name(rctx)
if os_name != "windows":
# On Unix-like systems, recursively add read permissions to all files
# and ensure directories are traversable (need execute permission)
result = repo_utils.execute_unchecked(
rctx,
op = "Fixing wheel permissions {}".format(whl_path),
arguments = ["chmod", "-R", "a+rX", str(install_dir_path)],
logger = logger,
)
if result.return_code != 0:
# It's possible chmod is not available or the filesystem doesn't support it.
# This is fine, we just want to try to fix permissions if possible.
logger.warn(lambda: "Failed to fix file permissions: {}".format(result.stderr))
_maybe_fix_permissions(rctx, whl_path = whl_path, logger = logger)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, this permissions fixing is only needed on bazel 8.5 and below. The later versions do read-only permission fixes during extracting.


metadata_file = find_whl_metadata(
install_dir = install_dir_path,
logger = logger,
Expand Down Expand Up @@ -70,17 +56,35 @@ def whl_extract(rctx, *, whl_path, logger):
# The prefix does not exist in the wheel, we can continue
continue

for (src, dest) in merge_trees(src, rctx.path(dest_prefix)):
dest_dir = rctx.path(dest_prefix)
repo_utils.mkdir(rctx, dest_dir)
for (src, dest) in merge_trees(src, dest_dir):
logger.debug(lambda: "Renaming: {} -> {}".format(src, dest))
rctx.rename(src, dest)

# TODO @aignas 2025-12-16: when moving scripts to `bin`, rewrite the #!python
# shebang to be something else, for inspiration look at the hermetic
# toolchain wrappers
repo_utils.rename(rctx, src, dest)

# Ensure that there is no data dir left
rctx.delete(data_dir)

def _maybe_fix_permissions(rctx, *, whl_path, logger):
# Fix permissions on extracted files. Some wheels have files without read permissions set,
# which causes errors when trying to read them later.
# We apply this to the root directory to ensure that everything in bin/, site-packages/,
# etc. is readable and executable where appropriate.
os_name = repo_utils.get_platforms_os_name(rctx)
if os_name != "windows":
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, this permissions fixing is only needed on bazel 8.5 and below. The later versions do read-only permission fixes during extracting. This if statement could include the version checks as well.

# On Unix-like systems, recursively add read permissions to all files
# and ensure directories are traversable (need execute permission)
result = repo_utils.execute_unchecked(
rctx,
op = "Fixing wheel permissions {}".format(whl_path),
arguments = ["chmod", "-R", "a+rX", "."],
logger = logger,
)
if result.return_code != 0:
# It's possible chmod is not available or the filesystem doesn't support it.
# This is fine, we just want to try to fix permissions if possible.
logger.warn(lambda: "Failed to fix file permissions: {}".format(result.stderr))

def merge_trees(src, dest):
"""Merge src into the destination path.

Expand Down
Loading