fix: quote paths in stage1_bootstrap_template.sh (#4386)

Unquoted path expansions in `stage1_bootstrap_template.sh` fail with
word splitting when the executable symlink path, runfiles root
directory, or `RULES_PYTHON_EXTRACT_ROOT` contains spaces (for example,
in Bazel's `bazel_determinism_test`).

Quote `$0`, `$stub_filename`, `$RUNFILES_DIR`, `$PYTHON_BINARY`,
`$PYTHON_BINARY_ACTUAL`, `$RULES_PYTHON_EXTRACT_ROOT`, `$python_exe`,
and `$venv_site_packages` when resolving runfiles and creating runtime
venvs, and extend the bootstrap runfiles and runtime venv tests to
exercise paths with spaces.
diff --git a/news/4386.fixed.md b/news/4386.fixed.md
new file mode 100644
index 0000000..877e3e5
--- /dev/null
+++ b/news/4386.fixed.md
@@ -0,0 +1,3 @@
+(rules) Fixed stage 1 bootstrap script failing when the executable path,
+runfiles root, or `RULES_PYTHON_EXTRACT_ROOT` contains spaces
+([#4386](https://github.com/bazel-contrib/rules_python/pull/4386)).
diff --git a/python/private/stage1_bootstrap_template.sh b/python/private/stage1_bootstrap_template.sh
index ef09812..7f4ba8e 100644
--- a/python/private/stage1_bootstrap_template.sh
+++ b/python/private/stage1_bootstrap_template.sh
@@ -123,12 +123,12 @@
       if [[ ! -L "$stub_filename" ]]; then
         break
       fi
-      stub_filename=$(readlink $stub_filename)
+      stub_filename=$(readlink "$stub_filename")
     done
     echo >&2 "Unable to find runfiles directory for $1"
     exit 1
   }
-  RUNFILES_DIR=$(find_runfiles_root $0)
+  RUNFILES_DIR=$(find_runfiles_root "$0")
 fi
 
 if [[ -n "$RULES_PYTHON_TESTING_TELL_RUNFILES_ROOT" ]]; then
@@ -150,7 +150,7 @@
   fi
 }
 
-python_exe=$(find_python_interpreter $RUNFILES_DIR $PYTHON_BINARY)
+python_exe=$(find_python_interpreter "$RUNFILES_DIR" "$PYTHON_BINARY")
 
 # Zip files have to re-create the venv bin/python3 symlink because they
 # don't contain it already.
@@ -159,7 +159,7 @@
   use_exec=1
   # It should always be under runfiles, but double check this. We don't
   # want to accidentally create symlinks elsewhere.
-  if [[ "$python_exe" != $RUNFILES_DIR/* ]]; then
+  if [[ "$python_exe" != "$RUNFILES_DIR"/* ]]; then
     echo >&2 "ERROR: Program's venv binary not under runfiles: $python_exe"
     exit 1
   fi
@@ -179,15 +179,15 @@
     fi
   fi
   # The bin/ directory may not exist if it is empty.
-  mkdir -p "$(dirname $python_exe)"
+  mkdir -p "$(dirname "$python_exe")"
   _symlink "$symlink_to" "$python_exe"
 elif [[ "$RECREATE_VENV_AT_RUNTIME" == "1" ]]; then
   if [[ -n "$RULES_PYTHON_EXTRACT_ROOT" ]]; then
     use_exec=1
     # Use our runfiles path as a unique, reusable, location for the
     # binary-specific venv being created.
-    venv="$RULES_PYTHON_EXTRACT_ROOT/$(dirname $(dirname $PYTHON_BINARY))"
-    mkdir -p $RULES_PYTHON_EXTRACT_ROOT
+    venv="$RULES_PYTHON_EXTRACT_ROOT/$(dirname "$(dirname "$PYTHON_BINARY")")"
+    mkdir -p "$RULES_PYTHON_EXTRACT_ROOT"
   else
     # Re-exec'ing can't be used because we have to clean up the temporary
     # venv directory that is created.
@@ -199,7 +199,7 @@
   fi
 
   # Match the basename; some tools, e.g. pyvenv key off the executable name
-  python_exe="$venv/bin/$(basename $PYTHON_BINARY_ACTUAL)"
+  python_exe="$venv/bin/$(basename "$PYTHON_BINARY_ACTUAL")"
 
   if [[ ! -e "$python_exe" ]]; then
     if [[ "$PYTHON_BINARY_ACTUAL" == /* ]]; then
@@ -218,7 +218,7 @@
       fi
     fi
 
-    runfiles_venv="$RUNFILES_DIR/$(dirname $(dirname $PYTHON_BINARY))"
+    runfiles_venv="$RUNFILES_DIR/$(dirname "$(dirname "$PYTHON_BINARY")")"
     # When RESOLVE_PYTHON_BINARY_AT_RUNTIME is true, it means the toolchain
     # has thrown two complications at us:
     # 1. The build-time assumption of the Python version may not match the
@@ -256,7 +256,7 @@
     _symlink "$python_exe_actual" "$python_exe"
 
     if [[ ! -e "$venv_site_packages" ]]; then
-      mkdir -p $(dirname $venv_site_packages)
+      mkdir -p "$(dirname "$venv_site_packages")"
       _symlink "$runfiles_venv_site_packages" "$venv_site_packages"
     fi
   fi
@@ -273,7 +273,7 @@
 if [[ ! -x "$python_exe" ]]; then
   if [[ ! -e "$python_exe" ]]; then
     echo >&2 "ERROR: Python interpreter not found: $python_exe"
-    ls -l $python_exe >&2
+    ls -l "$python_exe" >&2
     exit 1
   elif [[ ! -x "$python_exe" ]]; then
     echo >&2 "ERROR: Python interpreter not executable: $python_exe"
diff --git a/tests/bootstrap_impls/run_binary_find_runfiles_test.sh b/tests/bootstrap_impls/run_binary_find_runfiles_test.sh
index a6c1b56..a5deea8 100755
--- a/tests/bootstrap_impls/run_binary_find_runfiles_test.sh
+++ b/tests/bootstrap_impls/run_binary_find_runfiles_test.sh
@@ -30,14 +30,20 @@
   exit 1
 fi
 
-bin_link_layer_1=$TEST_TMPDIR/link1
+bin_link_layer_1="$TEST_TMPDIR/link with spaces 1"
 ln -s "$bin" "$bin_link_layer_1"
-bin_link_layer_2=$TEST_TMPDIR/link2
+bin_link_layer_2="$TEST_TMPDIR/link with spaces 2"
 ln -s "$bin_link_layer_1" "$bin_link_layer_2"
 
-result=$(RUNFILES_DIR='' RUNFILES_MANIFEST_FILE='' $bin)
-result_link_layer_1=$(RUNFILES_DIR='' RUNFILES_MANIFEST_FILE='' $bin_link_layer_1)
-result_link_layer_2=$(RUNFILES_DIR='' RUNFILES_MANIFEST_FILE='' $bin_link_layer_2)
+space_dir="$TEST_TMPDIR/dir with spaces"
+mkdir -p "$space_dir"
+runfiles_with_spaces="$space_dir/test.runfiles"
+ln -s "${bin%.runfiles*}.runfiles" "$runfiles_with_spaces"
+
+result=$(RUNFILES_DIR='' RUNFILES_MANIFEST_FILE='' "$bin")
+result_link_layer_1=$(RUNFILES_DIR='' RUNFILES_MANIFEST_FILE='' "$bin_link_layer_1")
+result_link_layer_2=$(RUNFILES_DIR='' RUNFILES_MANIFEST_FILE='' "$bin_link_layer_2")
+result_space_runfiles=$(RUNFILES_DIR="$runfiles_with_spaces" RUNFILES_MANIFEST_FILE='' "$bin_link_layer_2")
 
 if [[ "$result" != "$result_link_layer_1" ]]; then
     echo "Output from test does not match output when invoked via a link;"
@@ -55,5 +61,10 @@
     echo "$result_link_layer_2"
     exit 1
 fi
+if ! grep -q "^Hello$" <<<"$result_space_runfiles"; then
+    echo "Expected output when invoked with RUNFILES_DIR containing spaces, got:"
+    echo "$result_space_runfiles"
+    exit 1
+fi
 
 exit 0
diff --git a/tests/bootstrap_impls/run_binary_venvs_use_declare_symlink_no_test.sh b/tests/bootstrap_impls/run_binary_venvs_use_declare_symlink_no_test.sh
index d484011..cfd5aa9 100755
--- a/tests/bootstrap_impls/run_binary_venvs_use_declare_symlink_no_test.sh
+++ b/tests/bootstrap_impls/run_binary_venvs_use_declare_symlink_no_test.sh
@@ -48,8 +48,8 @@
 expect_match "sys.executable:.*tmp.*python3" "$actual"
 
 # Now test that using a custom location for the bootstrap files works
-venvs_root=$(mktemp -d)
-actual=$(RULES_PYTHON_EXTRACT_ROOT=$venvs_root $bin)
+venvs_root=$(mktemp -d "$TEST_TMPDIR/venvs root.XXXXXX")
+actual=$(RULES_PYTHON_EXTRACT_ROOT="$venvs_root" "$bin")
 expect_match "sys.executable:.*$venvs_root" "$actual"
 
 # Exit if any of the expects failed