fix: run executable zip action with execution Python to better support windows building (#3890)

The legacy self-executable zip action invokes `cat` through `run_shell`.
The
action does not declare `cat`, and a Windows-hosted Bazel invocation
cannot use
that action reliably when the selected execution platform is Linux.

Run the existing `exe_zip_maker` through `actions_run` instead. This
selects the
helper and Python runtime from the execution configuration, preserves
the
prelude-plus-zip output format, and removes the ambient shell utility
dependency. A news entry documents the fix.

The Bazel integration that motivated this fix is

[bazelbuild/bazel#30121](https://github.com/bazelbuild/bazel/pull/30121).

---------

Co-authored-by: Richard Levasseur <richardlev@gmail.com>
(cherry picked from commit 1d99f70e5967da59ecd948c0e6775e1e6424d889)

Work towards #3867
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d585ce8..5819796 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,8 @@
 * Fixed a flaky error on Windows 2022 when looking up the win32 version during
 site initialization by retrying the lookup
 ([#3721](https://github.com/bazel-contrib/rules_python/issues/3721)).
+* (binaries) Fixed building of legacy zipapps on Windows execution platforms by
+using a hermetic tool instead of host `cat`.
 * (bootstrap) Fixed stage 1 bootstrap imports when target outputs shadow standard
 library modules.
 * (coverage) Skip lcov report when no data was collected.
@@ -78,6 +80,7 @@
 
 
 
+
 {#v2-1-0}
 ## [2.1.0] - 2026-06-17
 
diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index 11246ec..b15d1d2 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -38,6 +38,7 @@
 load(
     ":common.bzl",
     "ExplicitSymlink",
+    "actions_run",
     "collect_cc_info",
     "collect_deps",
     "collect_imports",
@@ -218,6 +219,10 @@
             default = "//python/private:debugger_if_target_config",
             providers = [PyInfo],
         ),
+        "_exe_zip_maker": lambda: attrb.Label(
+            cfg = "exec",
+            default = "//tools/private/zipapp:exe_zip_maker",
+        ),
         "_launcher": lambda: attrb.Label(
             cfg = "target",
             # NOTE: This is an executable, but is only used for Windows. It
@@ -1094,15 +1099,16 @@
     else:
         ctx.actions.write(prelude, "#!/usr/bin/env python3\n")
 
-    ctx.actions.run_shell(
-        command = "cat {prelude} {zip} > {output}".format(
-            prelude = prelude.path,
-            zip = zip_file.path,
-            output = output.path,
-        ),
-        inputs = [prelude, zip_file],
+    args = ctx.actions.args()
+    args.add(prelude)
+    args.add(zip_file)
+    args.add(output)
+    actions_run(
+        ctx,
+        executable = ctx.attr._exe_zip_maker,
+        arguments = [args],
+        inputs = depset([prelude, zip_file]),
         outputs = [output],
-        use_default_shell_env = True,
         mnemonic = "PyBuildExecutableZip",
         progress_message = "Build Python zip executable: %{label}",
     )