fix(bootstrap): manual runfiles path construction when using submodules (#3636)

This fixes https://github.com/bazel-contrib/rules_python/issues/3563.
Verified by running the repro in
https://github.com/mering/reproduction_rules_python_1_7.

The identified regression in
https://github.com/bazel-contrib/rules_python/commit/b8e32c454a1158cd78ce4ecaef809b99bef4e5da
is problematic because prepending `ctx.workspace_name` to `short_path`
results in paths containing `..` (e.g., `_main/../sub+/path/to/file.py`)
when building from a root module that includes other modules. This
causes the `_find_runfiles_root`
[logic](https://github.com/faximan/rules_python/blob/eb6ac472eb86cc263acc336a2b73982043069aae/python/private/site_init_template.py#L73),
which _counts slashes_, to incorrectly calculate the runfiles root.

The fix is simply using the available `runfiles_root_path` function
instead. In the example above, this makes the path simply
`sub+/path/to/file.py`.

---------

Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e574870..031b066 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -80,6 +80,9 @@
   configuration. See the {any}`RULES_PYTHON_PYCACHE_DIR` environment variable
   for more information.
   ([#3643](https://github.com/bazel-contrib/rules_python/issues/3643)).
+* (bootstrap) Fixed incorrect runfiles path construction in bootstrap
+  scripts when binary is defined in another bazel module
+  ([#3563](https://github.com/bazel-contrib/rules_python/issues/3563)).
 
 {#v0-0-0-added}
 ### Added
diff --git a/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel b/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel
index 53344c7..318822d 100644
--- a/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel
+++ b/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel
@@ -1,5 +1,6 @@
 load("@rules_python//python:py_binary.bzl", "py_binary")
 load("@rules_python//python:py_library.bzl", "py_library")
+load("@rules_python//python/zipapp:py_zipapp_binary.bzl", "py_zipapp_binary")
 
 py_library(
     name = "lib",
@@ -26,4 +27,13 @@
     ],
 )
 
+# This is used for regression testing runfiles paths in submodules.
+# https://github.com/bazel-contrib/rules_python/issues/3563.
+py_zipapp_binary(
+    name = "bin_zipapp",
+    testonly = True,
+    binary = ":bin",
+    visibility = ["//visibility:public"],
+)
+
 exports_files(["data/data.txt"])
diff --git a/examples/bzlmod/tests/other_module/BUILD.bazel b/examples/bzlmod/tests/other_module/BUILD.bazel
index 1bd8a90..24231e6 100644
--- a/examples/bzlmod/tests/other_module/BUILD.bazel
+++ b/examples/bzlmod/tests/other_module/BUILD.bazel
@@ -5,6 +5,7 @@
 # in the root module.
 
 load("@bazel_skylib//rules:build_test.bzl", "build_test")
+load("@rules_python//python:py_test.bzl", "py_test")
 
 build_test(
     name = "other_module_bin_build_test",
@@ -12,3 +13,16 @@
         "@our_other_module//other_module/pkg:bin",
     ],
 )
+
+py_test(
+    name = "other_module_import_test",
+    srcs = ["other_module_import_test.py"],
+    data = ["@our_other_module//other_module/pkg:bin_zipapp"],
+    env = {"ZIPAPP_PATH": "$(location @our_other_module//other_module/pkg:bin_zipapp)"},
+    # For now, skip this test on Windows because it fails for reasons
+    # other than the code path being tested.
+    target_compatible_with = select({
+        "@platforms//os:windows": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+)
diff --git a/examples/bzlmod/tests/other_module/other_module_import_test.py b/examples/bzlmod/tests/other_module/other_module_import_test.py
new file mode 100644
index 0000000..6b92a85
--- /dev/null
+++ b/examples/bzlmod/tests/other_module/other_module_import_test.py
@@ -0,0 +1,22 @@
+"""Regression test for https://github.com/bazel-contrib/rules_python/issues/3563"""
+import os
+import subprocess
+import sys
+
+def main():
+    # The rlocation path for the bin_zipapp. It is in the "our_other_module" repository.
+    zipapp_path = os.environ.get("ZIPAPP_PATH")
+    print(f"Running bin_zipapp at: {zipapp_path}")
+
+    result = subprocess.run([zipapp_path], capture_output=True, text=True)
+    print("--- bin_zippapp stdout ---")
+    print(result.stdout)
+    print("--- bin_zippapp stderr ---")
+    print(result.stderr)
+
+    if result.returncode != 0:
+        print(f"bin_zippapp failed with return code {result.returncode}")
+        sys.exit(result.returncode)
+
+if __name__ == "__main__":
+    main()
diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index 284aea6..495c7dd 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -510,10 +510,7 @@
         substitutions = {
             "%python_binary%": python_binary,
             "%python_binary_actual%": python_binary_actual,
-            "%stage2_bootstrap%": "{}/{}".format(
-                ctx.workspace_name,
-                stage2_bootstrap.short_path,
-            ),
+            "%stage2_bootstrap%": runfiles_root_path(ctx, stage2_bootstrap.short_path),
             "%workspace_name%": ctx.workspace_name,
         },
     )
@@ -616,7 +613,7 @@
             "%add_runfiles_root_to_sys_path%": add_runfiles_root_to_sys_path,
             "%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
             "%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False",
-            "%site_init_runfiles_path%": "{}/{}".format(ctx.workspace_name, site_init.short_path),
+            "%site_init_runfiles_path%": runfiles_root_path(ctx, site_init.short_path),
             "%workspace_name%": ctx.workspace_name,
         },
         computed_substitutions = computed_subs,
@@ -671,10 +668,7 @@
     if (ctx.configuration.coverage_enabled and
         runtime and
         runtime.coverage_tool):
-        return "{}/{}".format(
-            ctx.workspace_name,
-            runtime.coverage_tool.short_path,
-        )
+        return runfiles_root_path(ctx, runtime.coverage_tool.short_path)
     else:
         return ""
 
@@ -777,10 +771,7 @@
         if (ctx.configuration.coverage_enabled and
             runtime and
             runtime.coverage_tool):
-            coverage_tool_runfiles_path = "{}/{}".format(
-                ctx.workspace_name,
-                runtime.coverage_tool.short_path,
-            )
+            coverage_tool_runfiles_path = runfiles_root_path(ctx, runtime.coverage_tool.short_path)
         else:
             coverage_tool_runfiles_path = ""
         if runtime:
@@ -793,7 +784,7 @@
         subs["%coverage_tool%"] = coverage_tool_runfiles_path
         subs["%import_all%"] = ("True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False")
         subs["%imports%"] = ":".join(imports.to_list())
-        subs["%main%"] = "{}/{}".format(ctx.workspace_name, main_py.short_path)
+        subs["%main%"] = runfiles_root_path(ctx, main_py.short_path)
 
     ctx.actions.expand_template(
         template = template,