fix (venv_site_packages): Fix wrong runfiles.symlinks when py_binary is not in root module (#3505)

When: 1) venv_site_packages is on 2) we have a py_binary in a non-root
module (e.g. a tool used in rules), and it uses python deps that result
in usage of runfiles.symlinks, the `ctx.runfile` based symlinks end up
going in the wrong folder (`_main`), while the
`ctx.actions.symlink(...)` files go in the right place. This results in
an invalid `venv` and import errors.

The reason is that `actions.symlinks` always go to the `_main` module,
as [Bazel docs
explain](https://bazel.build/extending/rules#runfiles_symlinks). To send
symlinks to other modules, one needs to use root_symlinks and prefix
them with the right module name.

Fixes: https://github.com/bazel-contrib/rules_python/issues/3503

---------

Co-authored-by: Shayan Hoshyari <hoshyari@adobe.com>
Co-authored-by: Richard Levasseur <rlevasseur@google.com>
diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index 1b884e9..5bac624 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -629,8 +629,10 @@
         ),
         # venv files for user library dependencies (files that are specific
         # to the executable bootstrap and python runtime aren't here).
+        # `root_symlinks` should be used, otherwise, with symlinks files always go
+        # to `_main` prefix, and binaries from non-root module become broken.
         lib_runfiles = ctx.runfiles(
-            symlinks = venv_app_files.runfiles_symlinks,
+            root_symlinks = venv_app_files.runfiles_symlinks,
         ),
     )
 
diff --git a/python/private/venv_runfiles.bzl b/python/private/venv_runfiles.bzl
index 02b18e5..7f6af0c 100644
--- a/python/private/venv_runfiles.bzl
+++ b/python/private/venv_runfiles.bzl
@@ -65,7 +65,10 @@
             bin_venv_path = paths.join(base, venv_path)
             if is_file(link_to):
                 # use paths.join to handle ctx.label.package = ""
-                symlink_from = paths.join(ctx.label.package, bin_venv_path)
+                # runfile_prefix should be prepended as we use runfiles.root_symlinks
+                runfile_prefix = ctx.label.repo_name or ctx.workspace_name
+                symlink_from = paths.join(runfile_prefix, ctx.label.package, bin_venv_path)
+
                 runfiles_symlinks[symlink_from] = link_to
             else:
                 venv_link = ctx.actions.declare_symlink(bin_venv_path)
diff --git a/tests/modules/other/BUILD.bazel b/tests/modules/other/BUILD.bazel
index 46f1b96..665049b 100644
--- a/tests/modules/other/BUILD.bazel
+++ b/tests/modules/other/BUILD.bazel
@@ -1,3 +1,4 @@
+load("@rules_python//python:py_binary.bzl", "py_binary")
 load("@rules_python//tests/support:py_reconfig.bzl", "py_reconfig_binary")
 
 package(
@@ -12,3 +13,18 @@
     bootstrap_impl = "system_python",
     main = "external_main.py",
 )
+
+py_binary(
+    name = "venv_bin",
+    srcs = ["venv_bin.py"],
+    config_settings = {
+        "@rules_python//python/config_settings:bootstrap_impl": "script",
+        "@rules_python//python/config_settings:venvs_site_packages": "yes",
+    },
+    deps = [
+        # Add two packages that install into the same directory. This is
+        # to test that namespace packages install correctly and are importable.
+        "//nspkg_delta",
+        "//nspkg_gamma",
+    ],
+)
diff --git a/tests/modules/other/venv_bin.py b/tests/modules/other/venv_bin.py
new file mode 100644
index 0000000..5455b23
--- /dev/null
+++ b/tests/modules/other/venv_bin.py
@@ -0,0 +1,17 @@
+import nspkg
+
+print(nspkg)
+
+import nspkg.subnspkg
+
+print(nspkg.subnspkg)
+
+import nspkg.subnspkg.delta
+
+print(nspkg.subnspkg.delta)
+
+import nspkg.subnspkg.gamma
+
+print(nspkg.subnspkg.gamma)
+
+print("@other//:venv_bin ran successfully.")
diff --git a/tests/venv_site_packages_libs/BUILD.bazel b/tests/venv_site_packages_libs/BUILD.bazel
index 2eb9678..e573dc6 100644
--- a/tests/venv_site_packages_libs/BUILD.bazel
+++ b/tests/venv_site_packages_libs/BUILD.bazel
@@ -1,3 +1,4 @@
+load("@rules_shell//shell:sh_test.bzl", "sh_test")
 load("//python:py_library.bzl", "py_library")
 load("//tests/support:py_reconfig.bzl", "py_reconfig_test")
 load(
@@ -19,6 +20,20 @@
     ],
 )
 
+sh_test(
+    name = "py_binary_other_module_test",
+    srcs = [
+        "py_binary_other_module_test.sh",
+    ],
+    data = [
+        "@other//:venv_bin",
+    ],
+    env = {
+        "VENV_BIN": "$(rootpath @other//:venv_bin)",
+    },
+    target_compatible_with = NOT_WINDOWS,
+)
+
 py_reconfig_test(
     name = "venvs_site_packages_libs_test",
     srcs = ["bin.py"],
diff --git a/tests/venv_site_packages_libs/py_binary_other_module_test.sh b/tests/venv_site_packages_libs/py_binary_other_module_test.sh
new file mode 100755
index 0000000..9fb71d5
--- /dev/null
+++ b/tests/venv_site_packages_libs/py_binary_other_module_test.sh
@@ -0,0 +1,6 @@
+# Test that for a py_binary from a dependency module, we place links created via
+# runfiles(...) in the right place. This tests the fix made for issues/3503
+
+set -eu
+echo "[*] Testing running the binary"
+"$VENV_BIN"