-
-
Notifications
You must be signed in to change notification settings - Fork 681
feat(venv): make wheel scripts runnable in venv #3743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b4e6496
c676d6e
10ace3b
0fd7c6d
7d2893f
16c5100
9e53c53
76cc0e0
9470aac
5b57de2
103ec9a
f41bfda
fe1ad5f
b35951a
52dfac5
45611a5
fd6cda6
958e61e
194693c
82b9acc
dd2b0b3
3ed727e
7561c72
24a94a2
1fabeea
473966b
dd5d8c2
08aeea4
747f7cb
03da47f
de2a11f
82e9e2a
cdb2cf1
e7ca80d
e16757e
e35d1a4
e38fd1c
9184bcc
db05136
51f614b
9681318
50cc8f0
cd18acc
8c3daaa
835d0a9
f08b096
ec6926e
181c4a5
827baf2
5894daf
36b8dbd
85ea206
cfefcfb
61bef05
47d4aae
6a3dda0
8012fbb
0c9a831
ec2cac0
a7c586c
2271e18
02be9e0
e276160
da40401
ad1202a
8f97ae3
a5eda25
62a50c9
b9eb87e
89c1a5a
cf5ab40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() |
| 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}()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| #!/bin/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}()) | ||
| 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() |
| 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) |
| 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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -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": | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| # 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. | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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/bashshould be replaced with/usr/bin/env bashon those setups, so I am wondering if it is similar for sh.