fix: handle unsubstituted template placeholders for external native py_binary by thomasdesr · Pull Request #3495 · bazel-contrib/rules_python

and others added 2 commits

January 5, 2026 16:27
…y_binary

Problem:
In rules_python 1.7.0, PR bazel-contrib#3334 ("build: Starlarkify python flags") introduced
new template placeholders in the bootstrap scripts: %stage2_bootstrap% and
%interpreter_args%. These placeholders are expected to be substituted by
rules_python's Starlark code when building py_binary targets.

However, when external repositories (like gRPC's cython) define py_binary
using the native rule, these placeholders are NOT substituted. In Bazel 7+,
native py_binary is implemented by rules_python, but native py_binary doesn't
expose attributes like `interpreter_args` that the substitution logic expects.
The result is that literal placeholder text ends up in the generated bootstrap
scripts, causing Python SyntaxError or file-not-found errors at runtime:

    File ".../cython_binary", line 39
      %interpreter_args%
      ^
    SyntaxError: invalid syntax

Fix:
Use a sentinel detection pattern directly in the templates. The key insight is
that by splitting the sentinel string (e.g., "%stage2" + "_bootstrap%"), the
substitution logic won't replace it since it looks for the exact contiguous
string. At runtime, the concatenation produces the original placeholder text,
which we compare against to detect if substitution occurred.

For %stage2_bootstrap%, we fall back to %main% which IS substituted even for
native py_binary. For %interpreter_args%, we wrap it in triple-quotes so it's
always valid Python syntax, then detect the sentinel and default to an empty
list.

This is a template-side fix that is backwards compatible and doesn't require
changes to Bazel or the substitution logic.

Test:
Add an integration test that creates an external repository with a native
py_binary (exactly like gRPC and other external repos do) and verifies it
can be built and executed successfully in WORKSPACE mode.
- Add STAGE2_BOOTSTRAP validation to Python template (consistency with shell)
- Add comments explaining sentinel pattern in both templates
- Restore diagnostic output in test script

@thomasdesr

gemini-code-assist[bot]

The sh_test requires @rules_shell which isn't provided transitively.
Buildifier auto-adds the load statement for rules_shell's sh_test,
so we need the dependency in the standalone WORKSPACE.
Bazel 9 removed native py_binary from the default namespace when
using bzlmod, making this test incompatible. Restrict the test to
Bazel 7.4.1, 8.0.0, and self (current Bazel) where native rules
are still available.

thomasdesr added a commit to thomasdesr/rules_python_3495_repro that referenced this pull request

Jan 9, 2026
This demonstrates the regression in rules_python 1.7.0 where
%interpreter_args% template placeholders are not substituted for
external py_binary targets.

Building the same target now fails:

  bazel build @com_github_grpc_grpc//src/python/grpcio/grpc/_cython:cygrpc.pyx_cython_translation

Error:
  File ".../cython_binary", line 39
      %interpreter_args%
      ^
  SyntaxError: invalid syntax

Root cause: PR #3242 introduced new template variables that native
py_binary doesn't substitute.

Fix: bazel-contrib/rules_python#3495

thomasdesr added a commit to thomasdesr/rules_python_3495_repro that referenced this pull request

Jan 9, 2026
This demonstrates the regression in rules_python 1.7.0 where
%interpreter_args% template placeholders are not substituted for
external py_binary targets.

Building the same target now fails:

  bazel build @com_github_grpc_grpc//src/python/grpcio/grpc/_cython:cygrpc.pyx_cython_translation

Error:
  File ".../cython_binary", line 39
      %interpreter_args%
      ^
  SyntaxError: invalid syntax

Root cause: PR #3242 introduced new template variables that native
py_binary doesn't substitute.

Fix: bazel-contrib/rules_python#3495

rickeylev

@thomasdesr

Address review feedback to use simpler encoding for interpreter_args.
Instead of encoding as quoted Python strings that require ast.literal_eval
to parse, encode as plain newline-delimited values and parse with split().

This removes the ast import and simplifies the bootstrap template logic.

rickeylev