diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 630638ee80..92a9b233a8 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -227,10 +227,11 @@ tasks: # for now until we can look into it. build_targets: - "--" - - "..." - # As a regression test for #225, check that wheel targets still build when - # their package path is qualified with the repo name. - - "@rules_python//examples/wheel/..." + ##- "..." + ### As a regression test for #225, check that wheel targets still build when + ### their package path is qualified with the repo name. + ##- "@rules_python//examples/wheel/..." + - "//tests/py_zipapp:system_python_zipapp_test" build_flags: - "--noenable_bzlmod" - "--enable_workspace" @@ -238,7 +239,8 @@ tasks: - "--build_tag_filters=-integration-test" test_targets: - "--" - - "..." + ##- "..." + - "//tests/py_zipapp:system_python_zipapp_test" test_flags: - "--noenable_bzlmod" - "--enable_workspace" @@ -256,8 +258,14 @@ tasks: <<: *reusable_config name: "Default: Windows" platform: windows + build_targets: + - "--" + - "//tests/py_zipapp:system_python_zipapp_test" test_flags: - "--test_tag_filters=-integration-test,-fix-windows" + test_targets: + - "--" + - "//tests/py_zipapp:system_python_zipapp_test" rbe_min: <<: *minimum_supported_version <<: *reusable_config diff --git a/python/private/common.bzl b/python/private/common.bzl index ca201cb10f..6d9e0e3c84 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -18,7 +18,7 @@ load("@rules_cc//cc/common:cc_common.bzl", "cc_common") load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:py_interpreter_program.bzl", "PyInterpreterProgramInfo") -load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE") +load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE") load(":builders.bzl", "builders") load(":cc_helper.bzl", "cc_helper") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") @@ -56,6 +56,51 @@ def maybe_builtin_build_python_zip(value, settings = None): return settings +def _find_launcher_maker(ctx): + if config.bazel_9_or_later: + return (ctx.toolchains[LAUNCHER_MAKER_TOOLCHAIN_TYPE].binary, LAUNCHER_MAKER_TOOLCHAIN_TYPE) + return (ctx.executable._windows_launcher_maker, None) + +def create_windows_exe_launcher( + ctx, + *, + output, + python_binary_path, + use_zip_file): + """Creates a Windows exe launcher. + + Args: + ctx: The rule context. + output: The output file for the launcher. + python_binary_path: The path to the Python binary. + use_zip_file: Whether to use a zip file. + """ + launch_info = ctx.actions.args() + launch_info.use_param_file("%s", use_always = True) + launch_info.set_param_file_format("multiline") + launch_info.add("binary_type=Python") + launch_info.add(ctx.workspace_name, format = "workspace_name=%s") + launch_info.add( + "1" if py_internal.runfiles_enabled(ctx) else "0", + format = "symlink_runfiles_enabled=%s", + ) + launch_info.add(python_binary_path, format = "python_bin_path=%s") + launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s") + + launcher = ctx.attr._launcher[DefaultInfo].files_to_run.executable + executable, toolchain = _find_launcher_maker(ctx) + ctx.actions.run( + executable = executable, + arguments = [launcher.path, launch_info, output.path], + inputs = [launcher], + outputs = [output], + mnemonic = "PyBuildLauncher", + progress_message = "Creating launcher for %{label}", + # Needed to inherit PATH when using non-MSVC compilers like MinGW + use_default_shell_env = True, + toolchain = toolchain, + ) + def create_binary_semantics_struct( *, get_native_deps_dso_name, diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 5c3cb77c91..9f1113d9f6 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -45,6 +45,7 @@ load( "create_instrumented_files_info", "create_output_group_info", "create_py_info", + "create_windows_exe_launcher", "csv", "filter_to_py_srcs", "is_bool", @@ -63,7 +64,7 @@ load(":py_internal.bzl", "py_internal") load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG") load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo") load(":rule_builders.bzl", "ruleb") -load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE") +load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE") load(":transition_labels.bzl", "TRANSITION_LABELS") load(":venv_runfiles.bzl", "create_venv_app_files") @@ -71,7 +72,6 @@ _py_builtins = py_internal _EXTERNAL_PATH_PREFIX = "external" _ZIP_RUNFILES_DIRECTORY_NAME = "runfiles" _INIT_PY = "__init__.py" -_LAUNCHER_MAKER_TOOLCHAIN_TYPE = "@bazel_tools//tools/launcher:launcher_maker_toolchain_type" # Non-Google-specific attributes for executables # These attributes are for rules that accept Python sources. @@ -398,7 +398,7 @@ def _create_executable( else: bootstrap_output = executable else: - _create_windows_exe_launcher( + create_windows_exe_launcher( ctx, output = executable, use_zip_file = build_zip_enabled, @@ -789,43 +789,6 @@ def _create_stage1_bootstrap( is_executable = True, ) -def _find_launcher_maker(ctx): - if rp_config.bazel_9_or_later: - return (ctx.toolchains[_LAUNCHER_MAKER_TOOLCHAIN_TYPE].binary, _LAUNCHER_MAKER_TOOLCHAIN_TYPE) - return (ctx.executable._windows_launcher_maker, None) - -def _create_windows_exe_launcher( - ctx, - *, - output, - python_binary_path, - use_zip_file): - launch_info = ctx.actions.args() - launch_info.use_param_file("%s", use_always = True) - launch_info.set_param_file_format("multiline") - launch_info.add("binary_type=Python") - launch_info.add(ctx.workspace_name, format = "workspace_name=%s") - launch_info.add( - "1" if py_internal.runfiles_enabled(ctx) else "0", - format = "symlink_runfiles_enabled=%s", - ) - launch_info.add(python_binary_path, format = "python_bin_path=%s") - launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s") - - launcher = ctx.attr._launcher[DefaultInfo].files_to_run.executable - executable, toolchain = _find_launcher_maker(ctx) - ctx.actions.run( - executable = executable, - arguments = [launcher.path, launch_info, output.path], - inputs = [launcher], - outputs = [output], - mnemonic = "PyBuildLauncher", - progress_message = "Creating launcher for %{label}", - # Needed to inherit PATH when using non-MSVC compilers like MinGW - use_default_shell_env = True, - toolchain = toolchain, - ) - def _create_zip_file(ctx, *, output, zip_main, runfiles): """Create a Python zipapp (zip with __main__.py entry point).""" workspace_name = ctx.workspace_name @@ -1848,7 +1811,7 @@ def create_executable_rule_builder(implementation, **kwargs): ruleb.ToolchainType(TOOLCHAIN_TYPE), ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), ruleb.ToolchainType("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ] + ([ruleb.ToolchainType(_LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []), + ] + ([ruleb.ToolchainType(LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []), cfg = dict( implementation = _transition_executable_impl, inputs = TRANSITION_LABELS + [ diff --git a/python/private/toolchain_types.bzl b/python/private/toolchain_types.bzl index ef81bf3bd4..5b5cce90ee 100644 --- a/python/private/toolchain_types.bzl +++ b/python/private/toolchain_types.bzl @@ -21,3 +21,4 @@ implementation of the toolchain. TARGET_TOOLCHAIN_TYPE = Label("//python:toolchain_type") EXEC_TOOLS_TOOLCHAIN_TYPE = Label("//python:exec_tools_toolchain_type") PY_CC_TOOLCHAIN_TYPE = Label("//python/cc:toolchain_type") +LAUNCHER_MAKER_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/launcher:launcher_maker_toolchain_type") diff --git a/python/private/zipapp/py_zipapp_rule.bzl b/python/private/zipapp/py_zipapp_rule.bzl index cc399064ad..a0f0df943f 100644 --- a/python/private/zipapp/py_zipapp_rule.bzl +++ b/python/private/zipapp/py_zipapp_rule.bzl @@ -1,14 +1,15 @@ """Implementation of the zipapp rules.""" load("@bazel_skylib//lib:paths.bzl", "paths") +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:attributes.bzl", "apply_config_settings_attr") load("//python/private:builders.bzl", "builders") -load("//python/private:common.bzl", "BUILTIN_BUILD_PYTHON_ZIP", "actions_run", "maybe_builtin_build_python_zip", "maybe_create_repo_mapping", "runfiles_root_path") +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") load("//python/private:common_labels.bzl", "labels") load("//python/private:py_executable_info.bzl", "PyExecutableInfo") load("//python/private:py_internal.bzl", "py_internal") load("//python/private:py_runtime_info.bzl", "PyRuntimeInfo") -load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE") +load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE") load("//python/private:transition_labels.bzl", "TRANSITION_LABELS") def _is_symlink(f): @@ -18,13 +19,11 @@ def _is_symlink(f): return "-1" def _create_zipapp_main_py(ctx, py_runtime, py_executable, stage2_bootstrap): - python_exe = py_executable.venv_python_exe - if python_exe: - python_exe_path = runfiles_root_path(ctx, python_exe.short_path) - elif py_runtime.interpreter: - python_exe_path = runfiles_root_path(ctx, py_runtime.interpreter.short_path) + venv_python_exe = py_executable.venv_python_exe + if venv_python_exe: + venv_python_exe_path = runfiles_root_path(ctx, venv_python_exe.short_path) else: - python_exe_path = py_runtime.interpreter_path + venv_python_exe_path = "" if py_runtime.interpreter: 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): template = py_runtime.zip_main_template, output = zip_main_py, substitutions = { - "%python_binary%": python_exe_path, + "%python_binary%": venv_python_exe_path, "%python_binary_actual%": python_binary_actual_path, "%stage2_bootstrap%": runfiles_root_path(ctx, stage2_bootstrap.short_path), "%workspace_name%": ctx.workspace_name, @@ -184,20 +183,39 @@ def _py_zipapp_executable_impl(ctx): zip_file = _create_zip(ctx, py_runtime, py_executable, stage2_bootstrap) if ctx.attr.executable: - preamble = _create_shell_bootstrap(ctx, py_runtime, py_executable, stage2_bootstrap) - executable = _create_self_executable_zip(ctx, preamble, zip_file) - default_output = executable + if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints): + executable = ctx.actions.declare_file(ctx.label.name + ".exe") + + python_exe = py_executable.venv_python_exe + if python_exe: + python_exe_path = runfiles_root_path(ctx, python_exe.short_path) + elif py_runtime.interpreter: + python_exe_path = runfiles_root_path(ctx, py_runtime.interpreter.short_path) + else: + python_exe_path = py_runtime.interpreter_path + + create_windows_exe_launcher( + ctx, + output = executable, + python_binary_path = python_exe_path, + use_zip_file = True, + ) + default_outputs = [executable, zip_file] + else: + preamble = _create_shell_bootstrap(ctx, py_runtime, py_executable, stage2_bootstrap) + executable = _create_self_executable_zip(ctx, preamble, zip_file) + default_outputs = [executable] else: # Bazel requires executable=True rules to have an executable given, so give # a fake one to satisfy that. - default_output = zip_file + default_outputs = [zip_file] executable = ctx.actions.declare_file(ctx.label.name + "-not-executable") ctx.actions.write(executable, "echo 'ERROR: Non executable zip file'; exit 1") return [ DefaultInfo( - files = depset([default_output]), - runfiles = ctx.runfiles(files = [default_output]), + files = depset(default_outputs), + runfiles = ctx.runfiles(files = default_outputs), executable = executable, ), ] @@ -277,6 +295,18 @@ Whether the output should be an executable zip file. cfg = "exec", default = "//tools/private/zipapp:exe_zip_maker", ), + "_launcher": attr.label( + cfg = "target", + # NOTE: This is an executable, but is only used for Windows. It + # can't have executable=True because the backing target is an + # empty target for other platforms. + default = "//tools/launcher:launcher", + ), + "_windows_constraints": attr.label_list( + default = [ + "@platforms//os:windows", + ], + ), "_zip_shell_template": attr.label( default = ":zip_shell_template", allow_single_file = True, @@ -285,8 +315,15 @@ Whether the output should be an executable zip file. cfg = "exec", default = "//tools/private/zipapp:zipper", ), -} -_TOOLCHAINS = [EXEC_TOOLS_TOOLCHAIN_TYPE] +} | ({ + "_windows_launcher_maker": attr.label( + default = "@bazel_tools//tools/launcher:launcher_maker", + cfg = "exec", + executable = True, + ), +} if not rp_config.bazel_9_or_later else {}) + +_TOOLCHAINS = [EXEC_TOOLS_TOOLCHAIN_TYPE] + ([LAUNCHER_MAKER_TOOLCHAIN_TYPE] if rp_config.bazel_9_or_later else []) py_zipapp_binary = rule( doc = """ diff --git a/python/private/zipapp/zip_main_template.py b/python/private/zipapp/zip_main_template.py index 35db1645bc..2c26842e6c 100644 --- a/python/private/zipapp/zip_main_template.py +++ b/python/private/zipapp/zip_main_template.py @@ -30,7 +30,7 @@ # runfiles-root-relative path _STAGE2_BOOTSTRAP = "%stage2_bootstrap%" # runfiles-root-relative path to venv's bin/python3. Empty if venv not being used. -_PYTHON_BINARY = "%python_binary%" +_PYTHON_BINARY_VENV = "%python_binary%" # runfiles-root-relative path, absolute path, or single word. The actual Python # executable to use. _PYTHON_BINARY_ACTUAL = "%python_binary_actual%" @@ -106,11 +106,11 @@ def has_windows_executable_extension(path): if ( - _PYTHON_BINARY + _PYTHON_BINARY_VENV and is_windows() - and not has_windows_executable_extension(_PYTHON_BINARY) + and not has_windows_executable_extension(_PYTHON_BINARY_VENV) ): - _PYTHON_BINARY = _PYTHON_BINARY + ".exe" + _PYTHON_BINARY_VENV = _PYTHON_BINARY_VENV + ".exe" def search_path(name): @@ -124,14 +124,6 @@ def search_path(name): return None -def find_python_binary(module_space): - """Finds the real Python binary if it's not a normal absolute path.""" - if _PYTHON_BINARY: - return find_binary(module_space, _PYTHON_BINARY) - else: - return find_binary(module_space, _PYTHON_BINARY_ACTUAL) - - def find_binary(module_space, bin_name): """Finds the real binary if it's not a normal absolute path.""" if not bin_name: @@ -139,7 +131,7 @@ def find_binary(module_space, bin_name): if bin_name.startswith("//"): # Case 1: Path is a label. Not supported yet. raise AssertionError( - "Bazel does not support execution of Python interpreters via labels yet" + "Bazel does not support execution of Python interpreters via labels" ) elif os.path.isabs(bin_name): # Case 2: Absolute path. @@ -221,7 +213,7 @@ def execute_file( # - On Windows, os.execv doesn't handle arguments with spaces # correctly, and it actually starts a subprocess just like # subprocess.call. - # - When running in a workspace or zip file, we need to clean up the + # - When running in a zip file, we need to clean up the # workspace after the process finishes so control must return here. try: subprocess_argv = [python_program, main_filename] + args @@ -241,16 +233,18 @@ def main(): print_verbose("running zip main bootstrap") print_verbose("initial argv:", values=sys.argv) print_verbose("initial environ:", mapping=os.environ) - print_verbose("initial sys.executable", sys.executable) - print_verbose("initial sys.version", sys.version) + print_verbose("initial sys.executable:", sys.executable) + print_verbose("initial sys.version:", sys.version) + print_verbose("stage2_bootstrap:", _STAGE2_BOOTSTRAP) + print_verbose("python_binary_venv:", _PYTHON_BINARY_VENV) + print_verbose("python_binary_actual:", _PYTHON_BINARY_ACTUAL) + print_verbose("workspace_name:", _WORKSPACE_NAME) args = sys.argv[1:] new_env = {} # The main Python source file. - # The magic string percent-main-percent is replaced with the runfiles-relative - # filename of the main file of the Python binary in BazelPythonSemantics.java. main_rel_path = _STAGE2_BOOTSTRAP if is_windows(): main_rel_path = main_rel_path.replace("/", os.sep) @@ -273,38 +267,34 @@ def main(): "Cannot exec() %r: file not readable." % main_filename ) - python_program = find_python_binary(module_space) - if python_program is None: - raise AssertionError("Could not find python binary: " + _PYTHON_BINARY) - - # When a venv is used, the `bin/python3` symlink has to be recreated. - if _PYTHON_BINARY: - # The venv bin/python3 interpreter should always be under runfiles, but - # double check. We don't want to accidentally create symlinks elsewhere. - if not python_program.startswith(module_space): - raise AssertionError( - "Program's venv binary not under runfiles: {python_program}" - ) - - if os.path.isabs(_PYTHON_BINARY_ACTUAL): - symlink_to = _PYTHON_BINARY_ACTUAL - elif "/" in _PYTHON_BINARY_ACTUAL: - symlink_to = os.path.join(module_space, _PYTHON_BINARY_ACTUAL) - else: - symlink_to = search_path(_PYTHON_BINARY_ACTUAL) - if not symlink_to: + if _PYTHON_BINARY_VENV: + python_program = os.path.join(module_space, _PYTHON_BINARY_VENV) + # When a venv is used, the `bin/python3` symlink may need to be created. + # This case occurs when "create venv at runtime" or "resolve python at + # runtime" modes are enabled. + if not os.path.lexists(python_program): + # The venv bin/python3 interpreter should always be under runfiles, but + # double check. We don't want to accidentally create symlinks elsewhere + # or unlink outside our tree. + if not python_program.startswith(module_space): raise AssertionError( - f"Python interpreter to use not found on PATH: {_PYTHON_BINARY_ACTUAL}" + "Program's venv binary not under runfiles: {python_program}" ) + symlink_to = find_binary(module_space, _PYTHON_BINARY_ACTUAL) + os.makedirs(os.path.dirname(python_program), exist_ok=True) + try: + os.symlink(symlink_to, python_program) + except OSError as e: + raise Exception( + f"Unable to create venv python interpreter symlink: {python_program} -> {symlink_to}" + ) from e - # The bin/ directory may not exist if it is empty. - os.makedirs(os.path.dirname(python_program), exist_ok=True) - try: - os.symlink(symlink_to, python_program) - except OSError as e: - raise Exception( - f"Unable to create venv python interpreter symlink: {python_program} -> {symlink_to}" - ) from e + else: + python_program = find_binary(module_space, _PYTHON_BINARY_ACTUAL) + if python_program is None: + raise AssertionError( + "Could not find python binary: " + _PYTHON_BINARY_ACTUAL + ) # Some older Python versions on macOS (namely Python 3.7) may unintentionally # leave this environment variable set after starting the interpreter, which diff --git a/tests/py_zipapp/BUILD.bazel b/tests/py_zipapp/BUILD.bazel index 74df4aa04d..da42567656 100644 --- a/tests/py_zipapp/BUILD.bazel +++ b/tests/py_zipapp/BUILD.bazel @@ -1,9 +1,10 @@ +load("@rules_shell//shell:sh_test.bzl", "sh_test") load("//python:py_binary.bzl", "py_binary") +load("//python:py_library.bzl", "py_library") load("//python:py_test.bzl", "py_test") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//python/zipapp:py_zipapp_binary.bzl", "py_zipapp_binary") load("//tests/support:support.bzl", "NOT_WINDOWS") - # todo: add windows support. Windows support will be a bit odd. # It previously worked by having special logic in the exe launcher # that knew to look for .zip and running that through python @@ -17,7 +18,7 @@ py_binary( }, main = "main.py", target_compatible_with = NOT_WINDOWS, - deps = ["@dev_pip//absl_py"], + deps = [":some_dep"], ) py_zipapp_binary( @@ -45,15 +46,12 @@ py_binary( "//python/config_settings:venvs_site_packages": "no", }, main = "main.py", - # TODO: #2586 - Add windows support - target_compatible_with = NOT_WINDOWS, - deps = ["@dev_pip//absl_py"], + deps = [":some_dep"], ) py_zipapp_binary( name = "system_python_zipapp", binary = ":system_python_bin", - target_compatible_with = NOT_WINDOWS, ) py_test( @@ -63,5 +61,35 @@ py_test( env = { "TEST_ZIPAPP": "$(location :system_python_zipapp)", }, - target_compatible_with = NOT_WINDOWS, +) + +sh_test( + name = "system_python_zipapp_external_bootstrap_test", + srcs = ["system_python_zipapp_external_bootstrap_test.sh"], + data = [ + ":system_python_zipapp", + "//python:current_py_toolchain", + ], + env = { + "PYTHON": "$(PYTHON3_ROOTPATH)", + "ZIPAPP": "$(location :system_python_zipapp)", + }, + toolchains = ["//python:current_py_toolchain"], +) + +py_library( + name = "some_dep", + srcs = ["some_dep.py"], + experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", + imports = select({ + ":is_venvs_site_packages_enabled": ["tests/py_zipapp"], + "//conditions:default": ["."], + }), +) + +config_setting( + name = "is_venvs_site_packages_enabled", + flag_values = { + "//python/config_settings:venvs_site_packages": "yes", + }, ) diff --git a/tests/py_zipapp/main.py b/tests/py_zipapp/main.py index b8fdbe365e..8e67ec9fae 100644 --- a/tests/py_zipapp/main.py +++ b/tests/py_zipapp/main.py @@ -3,9 +3,18 @@ def main(): print("Hello from zipapp") - import absl + try: + import some_dep - print(f"absl: {absl}") + print(f"dep: {some_dep}") + except ImportError: + import sys + + print("Failed to import `some_dep`", file=sys.stderr) + print("sys.path:", file=sys.stderr) + for i, x in enumerate(sys.path): + print(i, x, file=sys.stderr) + raise if __name__ == "__main__": diff --git a/tests/py_zipapp/some_dep.py b/tests/py_zipapp/some_dep.py new file mode 100644 index 0000000000..b64ecfb84a --- /dev/null +++ b/tests/py_zipapp/some_dep.py @@ -0,0 +1 @@ +"""empty module""" diff --git a/tests/py_zipapp/system_python_zipapp_external_bootstrap_test.sh b/tests/py_zipapp/system_python_zipapp_external_bootstrap_test.sh new file mode 100755 index 0000000000..9546dae059 --- /dev/null +++ b/tests/py_zipapp/system_python_zipapp_external_bootstrap_test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# This test expects ZIPAPP env var to point to the zipapp file. +if [[ -z "${ZIPAPP:-}" ]]; then + echo "ZIPAPP env var not set" + exit 1 +fi + +# We're testing the invocation of `__main__.py`, so we have to +# manually pass the zipapp to python. +"$PYTHON" "$ZIPAPP" diff --git a/tests/py_zipapp/system_python_zipapp_test.py b/tests/py_zipapp/system_python_zipapp_test.py index 297a574f16..ec0f837135 100644 --- a/tests/py_zipapp/system_python_zipapp_test.py +++ b/tests/py_zipapp/system_python_zipapp_test.py @@ -5,20 +5,15 @@ class SystemPythonZipAppTest(unittest.TestCase): - def test_zipapp_contents(self): + def test_zipapp_runnable(self): zipapp_path = os.environ["TEST_ZIPAPP"] self.assertTrue(os.path.exists(zipapp_path)) self.assertTrue(os.path.isfile(zipapp_path)) - # The zipapp itself is a shell script prepended to the zip file. - with open(zipapp_path, "rb") as f: - content = f.read() - self.assertTrue(content.startswith(b"#!/usr/bin/env bash")) - output = subprocess.check_output([zipapp_path]).decode("utf-8").strip() self.assertIn("Hello from zipapp", output) - self.assertIn("absl", output) + self.assertIn("dep:", output) if __name__ == "__main__": diff --git a/tests/py_zipapp/venv_zipapp_test.py b/tests/py_zipapp/venv_zipapp_test.py index 40d20fedb4..9bb917156f 100644 --- a/tests/py_zipapp/venv_zipapp_test.py +++ b/tests/py_zipapp/venv_zipapp_test.py @@ -1,3 +1,4 @@ +import contextlib import os import subprocess import unittest @@ -5,24 +6,34 @@ class PyZipAppTest(unittest.TestCase): - def test_zipapp_contents(self): + def test_zipapp_runnable(self): zipapp_path = os.environ["TEST_ZIPAPP"] - self.assertTrue(os.path.exists(zipapp_path)) - self.assertTrue(os.path.isfile(zipapp_path)) - - # The zipapp itself is a shell script prepended to the zip file. - with open(zipapp_path, "rb") as f: - content = f.read() - self.assertTrue(content.startswith(b"#!/usr/bin/env bash")) - - output = subprocess.check_output([zipapp_path]).decode("utf-8").strip() + try: + output = ( + subprocess.check_output([zipapp_path], stderr=subprocess.STDOUT) + .decode("utf-8") + .strip() + ) + except subprocess.CalledProcessError as e: + self.fail( + ( + "exec failed: {}\n" + + "exit code: {}\n" + + "=== stdout/stderr start ===\n" + "{}\n" + "=== stdout/stderr end ===" + ).format(zipapp_path, e.returncode, e.output.decode("utf-8")) + ) self.assertIn("Hello from zipapp", output) - self.assertIn("absl", output) + self.assertIn("dep:", output) def assertHasPathMatchingSuffix(self, namelist, suffix, msg=None): if not any(name.endswith(suffix) for name in namelist): - self.fail(msg or f"No path in zipapp matching suffix '{suffix}'") + self.fail( + (msg or f"No path in zipapp matching suffix '{suffix}'") + + "\nAvailable paths:\n" + + "\n".join(namelist) + ) def assertZipEntryIsSymlink(self, zip_file, path, msg=None): try: @@ -39,10 +50,27 @@ def assertZipEntryIsSymlink(self, zip_file, path, msg=None): def _is_bzlmod_enabled(self): return os.environ["BZLMOD_ENABLED"] == "1" + @contextlib.contextmanager + def _open_zipapp(self, path): + zf = None + try: + try: + zf = zipfile.ZipFile(path, "r") + except zipfile.BadZipFile: + # On windows, the main output is the launcher .exe file, and the + # zip file is a sibling file. + path = path.replace(".exe", ".zip") + zf = zipfile.ZipFile(path, "r") + if zf: + yield zf + finally: + if zf: + zf.close() + def test_zipapp_structure(self): zipapp_path = os.environ["TEST_ZIPAPP"] - with zipfile.ZipFile(zipapp_path, "r") as zf: + with self._open_zipapp(zipapp_path) as zf: namelist = zf.namelist() if self._is_bzlmod_enabled():