fix(runfiles): correct Python runfiles path assumption (#3086)

The current _FindPythonRunfilesRoot() implementation assumes that
the Python module has been unpacked four levels below the runfiles
directory. This is not the case in multiple situations, for example when
rules_pycross is in use and has installed the module via pypi (in which
case it is five levels below runfiles).

Both strategies already know where the runfiles directory exists -
implement _GetRunfilesDir() on the _DirectoryBased strategy, then call
_GetRunfilesDir() in order to populate self._python_runfiles_dir.

Stop passing a bogus path to runfiles.Create() in
testCurrentRepository(),
such that the test actually uses the appropriate runfiles path.

Fixes #3085

---------

Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bae9823..9c6a6b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,10 @@
 ### Fixed
 * (gazelle) Remove {obj}`py_binary` targets with invalid `srcs`. This includes files
   that are not generated or regular files.
+* (runfiles) Fix incorrect Python runfiles path assumption - the existing
+  implementation assumes that it is always four levels below the runfiles
+  directory, leading to incorrect path checks
+  ([#3085](https://github.com/bazel-contrib/rules_python/issues/3085)).
 
 {#v0-0-0-added}
 ### Added
diff --git a/python/runfiles/runfiles.py b/python/runfiles/runfiles.py
index fc79427..bfa9d0d 100644
--- a/python/runfiles/runfiles.py
+++ b/python/runfiles/runfiles.py
@@ -229,6 +229,9 @@
         # runfiles strategy on those platforms.
         return posixpath.join(self._runfiles_root, path)
 
+    def _GetRunfilesDir(self) -> str:
+        return self._runfiles_root
+
     def EnvVars(self) -> Dict[str, str]:
         return {
             "RUNFILES_DIR": self._runfiles_root,
@@ -246,7 +249,7 @@
 
     def __init__(self, strategy: Union[_ManifestBased, _DirectoryBased]) -> None:
         self._strategy = strategy
-        self._python_runfiles_root = _FindPythonRunfilesRoot()
+        self._python_runfiles_root = strategy._GetRunfilesDir()
         self._repo_mapping = _RepositoryMapping.create_from_file(
             strategy.RlocationChecked("_repo_mapping")
         )
@@ -469,18 +472,6 @@
 _Runfiles = Runfiles
 
 
-def _FindPythonRunfilesRoot() -> str:
-    """Finds the root of the Python runfiles tree."""
-    root = __file__
-    # Walk up our own runfiles path to the root of the runfiles tree from which
-    # the current file is being run. This path coincides with what the Bazel
-    # Python stub sets up as sys.path[0]. Since that entry can be changed at
-    # runtime, we rederive it here.
-    for _ in range("rules_python/python/runfiles/runfiles.py".count("/") + 1):
-        root = os.path.dirname(root)
-    return root
-
-
 def CreateManifestBased(manifest_path: str) -> Runfiles:
     return Runfiles.CreateManifestBased(manifest_path)
 
diff --git a/tests/runfiles/BUILD.bazel b/tests/runfiles/BUILD.bazel
index 5c92026..84602d2 100644
--- a/tests/runfiles/BUILD.bazel
+++ b/tests/runfiles/BUILD.bazel
@@ -5,6 +5,9 @@
 py_test(
     name = "runfiles_test",
     srcs = ["runfiles_test.py"],
+    data = [
+        "//tests/support:current_build_settings",
+    ],
     env = {
         "BZLMOD_ENABLED": "1" if BZLMOD_ENABLED else "0",
     },
diff --git a/tests/runfiles/runfiles_test.py b/tests/runfiles/runfiles_test.py
index b8a3d5f..165ab8c 100644
--- a/tests/runfiles/runfiles_test.py
+++ b/tests/runfiles/runfiles_test.py
@@ -12,7 +12,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
 import os
+import pathlib
 import tempfile
 import unittest
 from typing import Any, List, Optional
@@ -63,6 +65,16 @@
             lambda: r.Rlocation("\\foo"),
         )
 
+    def testRlocationWithData(self) -> None:
+        r = runfiles.Create()
+        assert r is not None  # mypy doesn't understand the unittest api.
+        settings_path = r.Rlocation(
+            "rules_python/tests/support/current_build_settings.json"
+        )
+        assert settings_path is not None
+        settings = json.loads(pathlib.Path(settings_path).read_text())
+        self.assertIn("bootstrap_impl", settings)
+
     def testCreatesManifestBasedRunfiles(self) -> None:
         with _MockFile(contents=["a/b c/d"]) as mf:
             r = runfiles.Create(
@@ -692,7 +704,7 @@
             expected = ""
         else:
             expected = "rules_python"
-        r = runfiles.Create({"RUNFILES_DIR": "whatever"})
+        r = runfiles.Create()
         assert r is not None  # mypy doesn't understand the unittest api.
         self.assertEqual(r.CurrentRepository(), expected)
 
diff --git a/tests/runtime_env_toolchain/toolchain_runs_test.py b/tests/runtime_env_toolchain/toolchain_runs_test.py
index c66b0bb..6f0948f 100644
--- a/tests/runtime_env_toolchain/toolchain_runs_test.py
+++ b/tests/runtime_env_toolchain/toolchain_runs_test.py
@@ -10,10 +10,20 @@
 class RunTest(unittest.TestCase):
     def test_ran(self):
         rf = runfiles.Create()
-        settings_path = rf.Rlocation(
-            "rules_python/tests/support/current_build_settings.json"
-        )
+        try:
+            settings_path = rf.Rlocation(
+                "rules_python/tests/support/current_build_settings.json"
+            )
+        except ValueError as e:
+            # The current toolchain being used has a buggy zip file bootstrap, which
+            # leaves RUNFILES_DIR pointing at the first stage path and not the module
+            # path.
+            if platform.system() != "Windows" or "does not lie under the runfiles root" not in str(e):
+                raise e
+            settings_path = "./tests/support/current_build_settings.json"
+
         settings = json.loads(pathlib.Path(settings_path).read_text())
+
         if platform.system() == "Windows":
             self.assertEqual(
                 "/_magic_pyruntime_sentinel_do_not_use", settings["interpreter_path"]