From 7b6e573af759ce1f3db57121fe3f590c839bf545 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 23 Oct 2026 07:28:12 +0800 Subject: [PATCH 1/6] fix: recover cached venvs from dangling package symlinks --- python/private/python_bootstrap_template.txt | 8 +- tests/bootstrap_impls/BUILD.bazel | 7 ++ .../cached_venv_dangling_symlink_test.py | 84 +++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/bootstrap_impls/cached_venv_dangling_symlink_test.py diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index d3f26bbdc0..5f74c49d50 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -437,7 +437,13 @@ print(site.getsitepackages(["{venv_src}"])[-1]) for venv_rel_path, link_to_rf_path in RUNTIME_VENV_SYMLINKS.items(): venv_abs_path = join(venv, venv_rel_path) link_to = normpath(join(runfiles_root, link_to_rf_path)) - os.makedirs(dirname(venv_abs_path), exist_ok=True) + venv_parent = dirname(venv_abs_path) + if os.path.islink(venv_parent): + try: + os.stat(venv_parent) + except FileNotFoundError: + os.unlink(venv_parent) + os.makedirs(venv_parent, exist_ok=True) _symlink_exist_ok(from_=venv_abs_path, to=link_to) # Do this last to handle non-relative symlinks artifacts diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index 8417ec5b9f..6078e73bdd 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -140,6 +140,13 @@ pytest_test( ], ) +py_test( + name = "cached_venv_replaces_dangling_package_symlink_test", + srcs = ["cached_venv_dangling_symlink_test.py"], + data = ["//python/private:python_bootstrap_template.txt"], + deps = ["//python/runfiles"], +) + py_reconfig_test( name = "sys_path_order_bootstrap_system_python_test", srcs = ["sys_path_order_test.py"], diff --git a/tests/bootstrap_impls/cached_venv_dangling_symlink_test.py b/tests/bootstrap_impls/cached_venv_dangling_symlink_test.py new file mode 100644 index 0000000000..8e6f35b65a --- /dev/null +++ b/tests/bootstrap_impls/cached_venv_dangling_symlink_test.py @@ -0,0 +1,84 @@ +"""Regression test for dangling runtime package links in persistent venvs.""" + +from __future__ import annotations + +import os +import pathlib +import sys +import tempfile +import unittest + +from python.runfiles import runfiles + + +def _load_bootstrap() -> dict: + r = runfiles.Create() + if r: + template_path = r.Rlocation( + "rules_python/python/private/python_bootstrap_template.txt" + ) + template = pathlib.Path(template_path) + else: + template = ( + pathlib.Path(__file__).resolve().parents[2] + / "python/private/python_bootstrap_template.txt" + ) + source = template.read_text(encoding="utf-8") + for placeholder, value in [ + ("%shebang%", "#!/usr/bin/env python3"), + ("%stage2_bootstrap%", "unused.py"), + ("%runtime_venv_symlinks%", ""), + ]: + source = source.replace(placeholder, value) + + bootstrap = {"__name__": "bootstrap_test"} + saved_path = sys.path[:] + try: + exec(compile(source, str(template), "exec"), bootstrap) + finally: + sys.path[:] = saved_path + return bootstrap + + +class CachedVenvDanglingSymlinkTest(unittest.TestCase): + def test_replaces_dangling_package_link_before_creating_child_link(self) -> None: + bootstrap = _load_bootstrap() + with tempfile.TemporaryDirectory() as temp: + root = pathlib.Path(temp) + runfiles = root / "runfiles" + old_package = runfiles / "old/annotated_types" + old_package.mkdir(parents=True) + (old_package / "__init__.py").touch() + bootstrap.update( + EXTRACT_ROOT=str(root / "cache"), + PYTHON_BINARY="_main/_process.venv/Scripts/python.exe", + PYTHON_BINARY_ACTUAL=sys.executable, + RESOLVE_PYTHON_BINARY_AT_RUNTIME=False, + RUNTIME_VENV_SYMLINKS={ + "Lib/site-packages/annotated_types": "old/annotated_types", + }, + ) + + bootstrap["_create_venv"](str(runfiles), []) + old_package.rename(root / "retired-package") + + new_package = runfiles / "new/annotated_types" + new_package.mkdir(parents=True) + (new_package / "__init__.py").touch() + bootstrap["RUNTIME_VENV_SYMLINKS"] = { + "Lib/site-packages/annotated_types/__init__.py": + "new/annotated_types/__init__.py", + } + + bootstrap["_create_venv"](str(runfiles), []) + + package = root / "cache/_main/_process.venv/Lib/site-packages/annotated_types" + package_init = package / "__init__.py" + self.assertTrue(package.is_dir()) + self.assertFalse(package.is_symlink()) + self.assertTrue(package_init.is_symlink()) + self.assertTrue(os.path.samefile(package_init, new_package / "__init__.py")) + + +if __name__ == "__main__": + unittest.main() From f632a50b804e172fe7be910ef8c36ceb2dde957d Mon Sep 17 00:00:00 2001 From: Sean <22557931+sean0407@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:19:26 +0800 Subject: [PATCH 2/6] test: set explicit main for dangling venv test --- tests/bootstrap_impls/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index 6078e73bdd..40f60c135a 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -142,6 +142,7 @@ pytest_test( py_test( name = "cached_venv_replaces_dangling_package_symlink_test", + main = "cached_venv_dangling_symlink_test.py", srcs = ["cached_venv_dangling_symlink_test.py"], data = ["//python/private:python_bootstrap_template.txt"], deps = ["//python/runfiles"], From 3d94855f0796cf602c5e00349c6187637cb15a9e Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 24 Oct 2026 14:33:57 +0800 Subject: [PATCH 3/6] test: format dangling venv test target --- tests/bootstrap_impls/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index 40f60c135a..d839b59202 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -142,8 +142,8 @@ pytest_test( py_test( name = "cached_venv_replaces_dangling_package_symlink_test", - main = "cached_venv_dangling_symlink_test.py", srcs = ["cached_venv_dangling_symlink_test.py"], + main = "cached_venv_dangling_symlink_test.py", data = ["//python/private:python_bootstrap_template.txt"], deps = ["//python/runfiles"], ) From e6da0b7a2a77bfb4e7af3331685af9463d590035 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 24 Oct 2026 14:36:38 +0800 Subject: [PATCH 4/6] test: follow buildifier attribute order --- tests/bootstrap_impls/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index d839b59202..f8f3b6838d 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -143,8 +143,8 @@ pytest_test( py_test( name = "cached_venv_replaces_dangling_package_symlink_test", srcs = ["cached_venv_dangling_symlink_test.py"], - main = "cached_venv_dangling_symlink_test.py", data = ["//python/private:python_bootstrap_template.txt"], + main = "cached_venv_dangling_symlink_test.py", deps = ["//python/runfiles"], ) From b88ba42c1b201aa25f24c5215c852f09fc8190db Mon Sep 17 00:00:00 2001 From: Sean <22557931+sean0407@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:53:04 +0800 Subject: [PATCH 5/6] test: narrow runfile template path type --- tests/bootstrap_impls/cached_venv_dangling_symlink_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/bootstrap_impls/cached_venv_dangling_symlink_test.py b/tests/bootstrap_impls/cached_venv_dangling_symlink_test.py index 8e6f35b65a..a7f681cc3e 100644 --- a/tests/bootstrap_impls/cached_venv_dangling_symlink_test.py +++ b/tests/bootstrap_impls/cached_venv_dangling_symlink_test.py @@ -17,6 +17,7 @@ def _load_bootstrap() -> dict: template_path = r.Rlocation( "rules_python/python/private/python_bootstrap_template.txt" ) + assert template_path is not None template = pathlib.Path(template_path) else: template = ( From ad0a8b9a576fed5095f5d79925921a1e8b8954a7 Mon Sep 17 00:00:00 2001 From: Sean Li Date: Sun, 25 Oct 2026 22:16:17 +0800 Subject: [PATCH 6/6] fix: explain dangling cached venv symlink --- python/private/python_bootstrap_template.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 5f74c49d50..206c473a6a 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -438,6 +438,7 @@ print(site.getsitepackages(["{venv_src}"])[-1]) venv_abs_path = join(venv, venv_rel_path) link_to = normpath(join(runfiles_root, link_to_rf_path)) venv_parent = dirname(venv_abs_path) + # A cached venv can leave this parent as a dangling link to an old EXTRACT_ROOT. if os.path.islink(venv_parent): try: os.stat(venv_parent)