diff --git a/.bazelignore b/.bazelignore index 0384d0746e..913f1e419f 100644 --- a/.bazelignore +++ b/.bazelignore @@ -30,5 +30,6 @@ gazelle/examples/bzlmod_build_file_generation/bazel-bzlmod_build_file_generation gazelle/examples/bzlmod_build_file_generation/bazel-out gazelle/examples/bzlmod_build_file_generation/bazel-testlog tests/integration/compile_pip_requirements/bazel-compile_pip_requirements +tests/integration/external_native_py_binary/bazel-external_native_py_binary tests/integration/local_toolchains/bazel-local_toolchains tests/integration/py_cc_toolchain_registered/bazel-py_cc_toolchain_registered diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages index d11f96d664..1eae6fbad3 100644 --- a/.bazelrc.deleted_packages +++ b/.bazelrc.deleted_packages @@ -35,11 +35,12 @@ common --deleted_packages=gazelle/manifest/hasher common --deleted_packages=gazelle/manifest/test common --deleted_packages=gazelle/modules_mapping common --deleted_packages=gazelle/python -common --deleted_packages=gazelle/pythonconfig common --deleted_packages=gazelle/python/private +common --deleted_packages=gazelle/pythonconfig common --deleted_packages=tests/integration/compile_pip_requirements common --deleted_packages=tests/integration/compile_pip_requirements_test_from_external_repo common --deleted_packages=tests/integration/custom_commands +common --deleted_packages=tests/integration/external_native_py_binary common --deleted_packages=tests/integration/local_toolchains common --deleted_packages=tests/integration/pip_parse common --deleted_packages=tests/integration/pip_parse/empty diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 9717756036..06fe8fe62b 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -10,9 +10,27 @@ import sys import os import subprocess import uuid +import ast # runfiles-relative path +# NOTE: The sentinel strings are split (e.g., "%stage2" + "_bootstrap%") so that +# the substitution logic won't replace them. This allows runtime detection of +# unsubstituted placeholders, which occurs when native py_binary is used in +# external repositories. In that case, we fall back to %main% which Bazel's +# native rule does substitute. +_STAGE2_BOOTSTRAP_SENTINEL = "%stage2" + "_bootstrap%" STAGE2_BOOTSTRAP="%stage2_bootstrap%" +if STAGE2_BOOTSTRAP == _STAGE2_BOOTSTRAP_SENTINEL: + _MAIN_SENTINEL = "%main" + "%" + _main = "%main%" + if _main != _MAIN_SENTINEL and _main: + STAGE2_BOOTSTRAP = _main + else: + STAGE2_BOOTSTRAP = "" + +if not STAGE2_BOOTSTRAP: + print("ERROR: %stage2_bootstrap% (or %main%) was not substituted.", file=sys.stderr) + sys.exit(1) # runfiles-relative path to venv's python interpreter # Empty string if a venv is not setup. @@ -35,9 +53,17 @@ RECREATE_VENV_AT_RUNTIME="%recreate_venv_at_runtime%" WORKSPACE_NAME = "%workspace_name%" # Target-specific interpreter args. -INTERPRETER_ARGS = [ -%interpreter_args% -] +# Sentinel split to detect unsubstituted placeholder (see STAGE2_BOOTSTRAP above). +_INTERPRETER_ARGS_SENTINEL = "%interpreter" + "_args%" +_INTERPRETER_ARGS_RAW = """%interpreter_args%""".strip() +if _INTERPRETER_ARGS_RAW and _INTERPRETER_ARGS_RAW != _INTERPRETER_ARGS_SENTINEL: + INTERPRETER_ARGS = [ + ast.literal_eval(line.strip()) + for line in _INTERPRETER_ARGS_RAW.splitlines() + if line.strip() + ] +else: + INTERPRETER_ARGS = [] ADDITIONAL_INTERPRETER_ARGS = os.environ.get("RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS", "") diff --git a/python/private/stage1_bootstrap_template.sh b/python/private/stage1_bootstrap_template.sh index a984344647..cfc6f7e510 100644 --- a/python/private/stage1_bootstrap_template.sh +++ b/python/private/stage1_bootstrap_template.sh @@ -7,7 +7,22 @@ if [[ -n "${RULES_PYTHON_BOOTSTRAP_VERBOSE:-}" ]]; then fi # runfiles-relative path +# NOTE: The sentinel strings are split (e.g., "%stage2""_bootstrap%") so that +# the substitution logic won't replace them. This allows runtime detection of +# unsubstituted placeholders, which occurs when native py_binary is used in +# external repositories. In that case, we fall back to %main% which Bazel's +# native rule does substitute. +STAGE2_BOOTSTRAP_SENTINEL="%stage2""_bootstrap%" +MAIN_SENTINEL="%main""%" STAGE2_BOOTSTRAP="%stage2_bootstrap%" +MAIN="%main%" +if [[ "$STAGE2_BOOTSTRAP" == "$STAGE2_BOOTSTRAP_SENTINEL" ]]; then + if [[ "$MAIN" != "$MAIN_SENTINEL" && -n "$MAIN" ]]; then + STAGE2_BOOTSTRAP="$MAIN" + else + STAGE2_BOOTSTRAP="" + fi +fi # runfiles-relative path to python interpreter to use. # This is the `bin/python3` path in the binary's venv. @@ -35,6 +50,17 @@ VENV_REL_SITE_PACKAGES="%venv_rel_site_packages%" declare -a INTERPRETER_ARGS_FROM_TARGET=( %interpreter_args% ) +# Sentinel split to detect unsubstituted placeholder (see STAGE2_BOOTSTRAP above). +INTERPRETER_ARGS_SENTINEL="%interpreter""_args%" +if [[ "${#INTERPRETER_ARGS_FROM_TARGET[@]}" -eq 1 && + "${INTERPRETER_ARGS_FROM_TARGET[0]}" == "$INTERPRETER_ARGS_SENTINEL" ]]; then + INTERPRETER_ARGS_FROM_TARGET=() +fi + +if [[ -z "$STAGE2_BOOTSTRAP" ]]; then + echo >&2 "ERROR: %stage2_bootstrap% (or %main%) was not substituted." + exit 1 +fi if [[ "$IS_ZIPFILE" == "1" ]]; then # NOTE: Macs have an old version of mktemp, so we must use only the diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index f0f58daa3a..38e3466249 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -47,6 +47,18 @@ rules_python_integration_test( workspace_path = "compile_pip_requirements", ) +rules_python_integration_test( + name = "external_native_py_binary_workspace_test", + # Bazel 9+ removed native py_binary, so this test only works on older versions + bazel_versions = [ + "7.4.1", + "8.0.0", + "self", + ], + bzlmod = False, + workspace_path = "external_native_py_binary", +) + rules_python_integration_test( name = "local_toolchains_test", env = { diff --git a/tests/integration/external_native_py_binary/BUILD.bazel b/tests/integration/external_native_py_binary/BUILD.bazel new file mode 100644 index 0000000000..8e8e9c11c0 --- /dev/null +++ b/tests/integration/external_native_py_binary/BUILD.bazel @@ -0,0 +1,15 @@ +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +package(default_visibility = ["//visibility:public"]) + +# Test that a native py_binary from an external repo works. +# This exercises the bootstrap template's handling of unsubstituted placeholders. +sh_test( + name = "external_native_py_binary_test", + srcs = ["external_native_py_binary_test.sh"], + data = ["@native_py_binary_repo//:external_native_py_binary"], + env = { + "BIN_RLOCATION": "native_py_binary_repo/external_native_py_binary", + }, + deps = ["@bazel_tools//tools/bash/runfiles"], +) diff --git a/tests/integration/external_native_py_binary/WORKSPACE b/tests/integration/external_native_py_binary/WORKSPACE new file mode 100644 index 0000000000..afd1a082c1 --- /dev/null +++ b/tests/integration/external_native_py_binary/WORKSPACE @@ -0,0 +1,43 @@ +workspace(name = "external_native_py_binary") + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load( + "@bazel_tools//tools/build_defs/repo:local.bzl", + "local_repository", + "new_local_repository", +) + +http_archive( + name = "rules_shell", + sha256 = "3e114424a5c7e4fd43e0133cc6ecdfe54e45ae8affa14fadd839f29901424043", + strip_prefix = "rules_shell-0.4.0", + url = "https://github.com/bazelbuild/rules_shell/releases/download/v0.4.0/rules_shell-v0.4.0.tar.gz", +) + +local_repository( + name = "rules_python", + path = "../../..", +) + +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") + +py_repositories() + +python_register_toolchains( + name = "python_3_11", + python_version = "3.11", +) + +new_local_repository( + name = "native_py_binary_repo", + build_file_content = """ +package(default_visibility = ["//visibility:public"]) + +py_binary( + name = "external_native_py_binary", + srcs = ["main.py"], + main = "main.py", +) +""", + path = "external_repo", +) diff --git a/tests/integration/external_native_py_binary/external_native_py_binary_test.sh b/tests/integration/external_native_py_binary/external_native_py_binary_test.sh new file mode 100755 index 0000000000..8cf91745de --- /dev/null +++ b/tests/integration/external_native_py_binary/external_native_py_binary_test.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --- begin runfiles.bash initialization v3 --- +set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo >&2 "ERROR: cannot find $f"; exit 1; } +set -euo pipefail +# --- end runfiles.bash initialization v3 --- + +bin=$(rlocation "$BIN_RLOCATION") +output="$("$bin")" +if [[ "$output" != "external-native-ok" ]]; then + echo >&2 "Expected 'external-native-ok' but got: $output" + exit 1 +fi diff --git a/tests/integration/external_native_py_binary/external_repo/main.py b/tests/integration/external_native_py_binary/external_repo/main.py new file mode 100644 index 0000000000..50b3df29c9 --- /dev/null +++ b/tests/integration/external_native_py_binary/external_repo/main.py @@ -0,0 +1 @@ +print("external-native-ok")