fix: use `exec` to invoke the stage-2 bootstrap for non-zip case (#2047)

When the two-stage bootstrap is used, the parent shell process runs
python as a child
process, which changes how signals are propagated. Specifically, if a
signal is sent
_directly_ to the parent (e.g. `kill $parent`), the child process
(python) won't receive
it and it will appear to be ignored. This is because the parent process
is busy waiting
for the child process.

To fix, invoke the python process using `exec` instead. Because the
process is entirely
replaced, signals are sent directly to the replacement. This can't be
used for zip files,
though, because they rely on a catching the exit signal to perform
cleanup of the extracted
files.

Fixes https://github.com/bazelbuild/rules_python/issues/2043

---------

Co-authored-by: Richard Levasseur <rlevasseur@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac11e14..cc44a47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,9 @@
 * Nothing yet
 
 ### Fixed
+* (rules) Signals are properly received when using {obj}`--bootstrap_impl=script`
+  (for non-zip builds).
+  ([#2043](https://github.com/bazelbuild/rules_python/issues/2043))
 * (rules) Fixes python builds when the `--build_python_zip` is set to `false` on Windows. See [#1840](https://github.com/bazelbuild/rules_python/issues/1840).
 * (pip) Fixed pypi parse_simpleapi_html function for feeds with package metadata
   containing ">" sign
diff --git a/python/private/stage1_bootstrap_template.sh b/python/private/stage1_bootstrap_template.sh
index fb46cc6..48711aa 100644
--- a/python/private/stage1_bootstrap_template.sh
+++ b/python/private/stage1_bootstrap_template.sh
@@ -106,13 +106,28 @@
 interpreter_env+=("PYTHONSAFEPATH=1")
 
 export RUNFILES_DIR
-# NOTE: We use <(...) to pass the Python program as a file so that stdin can
-# still be passed along as normal.
-env \
-  "${interpreter_env[@]}" \
-  "$python_exe" \
-  "${interpreter_args[@]}" \
-  "$stage2_bootstrap" \
-  "$@"
 
-exit $?
+command=(
+  env
+  "${interpreter_env[@]}"
+  "$python_exe"
+  "${interpreter_args[@]}"
+  "$stage2_bootstrap"
+  "$@"
+)
+
+# We use `exec` instead of a child process so that signals sent directly (e.g.
+# using `kill`) to this process (the PID seen by the calling process) are
+# received by the Python process. Otherwise, this process receives the signal
+# and would have to manually propagate it.
+# See https://github.com/bazelbuild/rules_python/issues/2043#issuecomment-2215469971
+# for more information.
+#
+# However, when running a zip file, we need to clean up the workspace after the
+# process finishes so control must return here.
+if [[ "$IS_ZIPFILE" == "1" ]]; then
+  "${command[@]}"
+  exit $?
+else
+  exec "${command[@]}"
+fi