Skip to content

Commit 3aa6386

Browse files
authored
feat(zipapp): add windows support (#3561)
This adds Windows support to the py_zipapp rules. It works the same as what py_binary does: the Bazel builtin launcher.exe is populated with the path to the zip, and the exe then handles starting Python and running the zip. This should be the last of the implementation, so all that remains is deprecating the code in py_binary and removing it. Work towards #2586
1 parent 39e9a3f commit 3aa6386

File tree

11 files changed

+248
-135
lines changed

11 files changed

+248
-135
lines changed

python/private/common.bzl

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
1818
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
1919
load("@rules_python_internal//:rules_python_config.bzl", "config")
2020
load("//python/private:py_interpreter_program.bzl", "PyInterpreterProgramInfo")
21-
load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE")
21+
load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE")
2222
load(":builders.bzl", "builders")
2323
load(":cc_helper.bzl", "cc_helper")
2424
load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
@@ -56,6 +56,51 @@ def maybe_builtin_build_python_zip(value, settings = None):
5656

5757
return settings
5858

59+
def _find_launcher_maker(ctx):
60+
if config.bazel_9_or_later:
61+
return (ctx.toolchains[LAUNCHER_MAKER_TOOLCHAIN_TYPE].binary, LAUNCHER_MAKER_TOOLCHAIN_TYPE)
62+
return (ctx.executable._windows_launcher_maker, None)
63+
64+
def create_windows_exe_launcher(
65+
ctx,
66+
*,
67+
output,
68+
python_binary_path,
69+
use_zip_file):
70+
"""Creates a Windows exe launcher.
71+
72+
Args:
73+
ctx: The rule context.
74+
output: The output file for the launcher.
75+
python_binary_path: The path to the Python binary.
76+
use_zip_file: Whether to use a zip file.
77+
"""
78+
launch_info = ctx.actions.args()
79+
launch_info.use_param_file("%s", use_always = True)
80+
launch_info.set_param_file_format("multiline")
81+
launch_info.add("binary_type=Python")
82+
launch_info.add(ctx.workspace_name, format = "workspace_name=%s")
83+
launch_info.add(
84+
"1" if py_internal.runfiles_enabled(ctx) else "0",
85+
format = "symlink_runfiles_enabled=%s",
86+
)
87+
launch_info.add(python_binary_path, format = "python_bin_path=%s")
88+
launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s")
89+
90+
launcher = ctx.attr._launcher[DefaultInfo].files_to_run.executable
91+
executable, toolchain = _find_launcher_maker(ctx)
92+
ctx.actions.run(
93+
executable = executable,
94+
arguments = [launcher.path, launch_info, output.path],
95+
inputs = [launcher],
96+
outputs = [output],
97+
mnemonic = "PyBuildLauncher",
98+
progress_message = "Creating launcher for %{label}",
99+
# Needed to inherit PATH when using non-MSVC compilers like MinGW
100+
use_default_shell_env = True,
101+
toolchain = toolchain,
102+
)
103+
59104
def create_binary_semantics_struct(
60105
*,
61106
get_native_deps_dso_name,

python/private/py_executable.bzl

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ load(
4545
"create_instrumented_files_info",
4646
"create_output_group_info",
4747
"create_py_info",
48+
"create_windows_exe_launcher",
4849
"csv",
4950
"filter_to_py_srcs",
5051
"is_bool",
@@ -63,15 +64,14 @@ load(":py_internal.bzl", "py_internal")
6364
load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG")
6465
load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
6566
load(":rule_builders.bzl", "ruleb")
66-
load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE")
67+
load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE")
6768
load(":transition_labels.bzl", "TRANSITION_LABELS")
6869
load(":venv_runfiles.bzl", "create_venv_app_files")
6970

7071
_py_builtins = py_internal
7172
_EXTERNAL_PATH_PREFIX = "external"
7273
_ZIP_RUNFILES_DIRECTORY_NAME = "runfiles"
7374
_INIT_PY = "__init__.py"
74-
_LAUNCHER_MAKER_TOOLCHAIN_TYPE = "@bazel_tools//tools/launcher:launcher_maker_toolchain_type"
7575

7676
# Non-Google-specific attributes for executables
7777
# These attributes are for rules that accept Python sources.
@@ -398,7 +398,7 @@ def _create_executable(
398398
else:
399399
bootstrap_output = executable
400400
else:
401-
_create_windows_exe_launcher(
401+
create_windows_exe_launcher(
402402
ctx,
403403
output = executable,
404404
use_zip_file = build_zip_enabled,
@@ -789,43 +789,6 @@ def _create_stage1_bootstrap(
789789
is_executable = True,
790790
)
791791

792-
def _find_launcher_maker(ctx):
793-
if rp_config.bazel_9_or_later:
794-
return (ctx.toolchains[_LAUNCHER_MAKER_TOOLCHAIN_TYPE].binary, _LAUNCHER_MAKER_TOOLCHAIN_TYPE)
795-
return (ctx.executable._windows_launcher_maker, None)
796-
797-
def _create_windows_exe_launcher(
798-
ctx,
799-
*,
800-
output,
801-
python_binary_path,
802-
use_zip_file):
803-
launch_info = ctx.actions.args()
804-
launch_info.use_param_file("%s", use_always = True)
805-
launch_info.set_param_file_format("multiline")
806-
launch_info.add("binary_type=Python")
807-
launch_info.add(ctx.workspace_name, format = "workspace_name=%s")
808-
launch_info.add(
809-
"1" if py_internal.runfiles_enabled(ctx) else "0",
810-
format = "symlink_runfiles_enabled=%s",
811-
)
812-
launch_info.add(python_binary_path, format = "python_bin_path=%s")
813-
launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s")
814-
815-
launcher = ctx.attr._launcher[DefaultInfo].files_to_run.executable
816-
executable, toolchain = _find_launcher_maker(ctx)
817-
ctx.actions.run(
818-
executable = executable,
819-
arguments = [launcher.path, launch_info, output.path],
820-
inputs = [launcher],
821-
outputs = [output],
822-
mnemonic = "PyBuildLauncher",
823-
progress_message = "Creating launcher for %{label}",
824-
# Needed to inherit PATH when using non-MSVC compilers like MinGW
825-
use_default_shell_env = True,
826-
toolchain = toolchain,
827-
)
828-
829792
def _create_zip_file(ctx, *, output, zip_main, runfiles):
830793
"""Create a Python zipapp (zip with __main__.py entry point)."""
831794
workspace_name = ctx.workspace_name
@@ -1848,7 +1811,7 @@ def create_executable_rule_builder(implementation, **kwargs):
18481811
ruleb.ToolchainType(TOOLCHAIN_TYPE),
18491812
ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False),
18501813
ruleb.ToolchainType("@bazel_tools//tools/cpp:toolchain_type", mandatory = False),
1851-
] + ([ruleb.ToolchainType(_LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []),
1814+
] + ([ruleb.ToolchainType(LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []),
18521815
cfg = dict(
18531816
implementation = _transition_executable_impl,
18541817
inputs = TRANSITION_LABELS + [

python/private/toolchain_types.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ implementation of the toolchain.
2121
TARGET_TOOLCHAIN_TYPE = Label("//python:toolchain_type")
2222
EXEC_TOOLS_TOOLCHAIN_TYPE = Label("//python:exec_tools_toolchain_type")
2323
PY_CC_TOOLCHAIN_TYPE = Label("//python/cc:toolchain_type")
24+
LAUNCHER_MAKER_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/launcher:launcher_maker_toolchain_type")

python/private/zipapp/py_zipapp_rule.bzl

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Implementation of the zipapp rules."""
22

33
load("@bazel_skylib//lib:paths.bzl", "paths")
4+
load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
45
load("//python/private:attributes.bzl", "apply_config_settings_attr")
56
load("//python/private:builders.bzl", "builders")
6-
load("//python/private:common.bzl", "BUILTIN_BUILD_PYTHON_ZIP", "actions_run", "maybe_builtin_build_python_zip", "maybe_create_repo_mapping", "runfiles_root_path")
7+
load("//python/private:common.bzl", "BUILTIN_BUILD_PYTHON_ZIP", "actions_run", "create_windows_exe_launcher", "maybe_builtin_build_python_zip", "maybe_create_repo_mapping", "runfiles_root_path", "target_platform_has_any_constraint")
78
load("//python/private:common_labels.bzl", "labels")
89
load("//python/private:py_executable_info.bzl", "PyExecutableInfo")
910
load("//python/private:py_internal.bzl", "py_internal")
1011
load("//python/private:py_runtime_info.bzl", "PyRuntimeInfo")
11-
load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE")
12+
load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE")
1213
load("//python/private:transition_labels.bzl", "TRANSITION_LABELS")
1314

1415
def _is_symlink(f):
@@ -18,13 +19,11 @@ def _is_symlink(f):
1819
return "-1"
1920

2021
def _create_zipapp_main_py(ctx, py_runtime, py_executable, stage2_bootstrap):
21-
python_exe = py_executable.venv_python_exe
22-
if python_exe:
23-
python_exe_path = runfiles_root_path(ctx, python_exe.short_path)
24-
elif py_runtime.interpreter:
25-
python_exe_path = runfiles_root_path(ctx, py_runtime.interpreter.short_path)
22+
venv_python_exe = py_executable.venv_python_exe
23+
if venv_python_exe:
24+
venv_python_exe_path = runfiles_root_path(ctx, venv_python_exe.short_path)
2625
else:
27-
python_exe_path = py_runtime.interpreter_path
26+
venv_python_exe_path = ""
2827

2928
if py_runtime.interpreter:
3029
python_binary_actual_path = runfiles_root_path(ctx, py_runtime.interpreter.short_path)
@@ -36,7 +35,7 @@ def _create_zipapp_main_py(ctx, py_runtime, py_executable, stage2_bootstrap):
3635
template = py_runtime.zip_main_template,
3736
output = zip_main_py,
3837
substitutions = {
39-
"%python_binary%": python_exe_path,
38+
"%python_binary%": venv_python_exe_path,
4039
"%python_binary_actual%": python_binary_actual_path,
4140
"%stage2_bootstrap%": runfiles_root_path(ctx, stage2_bootstrap.short_path),
4241
"%workspace_name%": ctx.workspace_name,
@@ -184,20 +183,39 @@ def _py_zipapp_executable_impl(ctx):
184183

185184
zip_file = _create_zip(ctx, py_runtime, py_executable, stage2_bootstrap)
186185
if ctx.attr.executable:
187-
preamble = _create_shell_bootstrap(ctx, py_runtime, py_executable, stage2_bootstrap)
188-
executable = _create_self_executable_zip(ctx, preamble, zip_file)
189-
default_output = executable
186+
if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints):
187+
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
188+
189+
python_exe = py_executable.venv_python_exe
190+
if python_exe:
191+
python_exe_path = runfiles_root_path(ctx, python_exe.short_path)
192+
elif py_runtime.interpreter:
193+
python_exe_path = runfiles_root_path(ctx, py_runtime.interpreter.short_path)
194+
else:
195+
python_exe_path = py_runtime.interpreter_path
196+
197+
create_windows_exe_launcher(
198+
ctx,
199+
output = executable,
200+
python_binary_path = python_exe_path,
201+
use_zip_file = True,
202+
)
203+
default_outputs = [executable, zip_file]
204+
else:
205+
preamble = _create_shell_bootstrap(ctx, py_runtime, py_executable, stage2_bootstrap)
206+
executable = _create_self_executable_zip(ctx, preamble, zip_file)
207+
default_outputs = [executable]
190208
else:
191209
# Bazel requires executable=True rules to have an executable given, so give
192210
# a fake one to satisfy that.
193-
default_output = zip_file
211+
default_outputs = [zip_file]
194212
executable = ctx.actions.declare_file(ctx.label.name + "-not-executable")
195213
ctx.actions.write(executable, "echo 'ERROR: Non executable zip file'; exit 1")
196214

197215
return [
198216
DefaultInfo(
199-
files = depset([default_output]),
200-
runfiles = ctx.runfiles(files = [default_output]),
217+
files = depset(default_outputs),
218+
runfiles = ctx.runfiles(files = default_outputs),
201219
executable = executable,
202220
),
203221
]
@@ -277,6 +295,18 @@ Whether the output should be an executable zip file.
277295
cfg = "exec",
278296
default = "//tools/private/zipapp:exe_zip_maker",
279297
),
298+
"_launcher": attr.label(
299+
cfg = "target",
300+
# NOTE: This is an executable, but is only used for Windows. It
301+
# can't have executable=True because the backing target is an
302+
# empty target for other platforms.
303+
default = "//tools/launcher:launcher",
304+
),
305+
"_windows_constraints": attr.label_list(
306+
default = [
307+
"@platforms//os:windows",
308+
],
309+
),
280310
"_zip_shell_template": attr.label(
281311
default = ":zip_shell_template",
282312
allow_single_file = True,
@@ -285,8 +315,15 @@ Whether the output should be an executable zip file.
285315
cfg = "exec",
286316
default = "//tools/private/zipapp:zipper",
287317
),
288-
}
289-
_TOOLCHAINS = [EXEC_TOOLS_TOOLCHAIN_TYPE]
318+
} | ({
319+
"_windows_launcher_maker": attr.label(
320+
default = "@bazel_tools//tools/launcher:launcher_maker",
321+
cfg = "exec",
322+
executable = True,
323+
),
324+
} if not rp_config.bazel_9_or_later else {})
325+
326+
_TOOLCHAINS = [EXEC_TOOLS_TOOLCHAIN_TYPE] + ([LAUNCHER_MAKER_TOOLCHAIN_TYPE] if rp_config.bazel_9_or_later else [])
290327

291328
py_zipapp_binary = rule(
292329
doc = """

0 commit comments

Comments
 (0)