From 136c642e379e94ee3ed96652d1e45b13c0f5c8c6 Mon Sep 17 00:00:00 2001 From: Gleb Kolobkov Date: Tue, 23 Jun 2026 13:14:02 -0700 Subject: [PATCH 1/3] fix(rules): disambiguate slash target venv outputs --- news/slash-target-venv-output.fixed.md | 3 ++ python/private/py_executable.bzl | 5 +++- tests/base_rules/py_executable_base_tests.bzl | 30 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 news/slash-target-venv-output.fixed.md diff --git a/news/slash-target-venv-output.fixed.md b/news/slash-target-venv-output.fixed.md new file mode 100644 index 0000000000..780b498db7 --- /dev/null +++ b/news/slash-target-venv-output.fixed.md @@ -0,0 +1,3 @@ +(rules) Fixed venv output paths for `py_binary` and `py_test` targets whose +names contain path separators so distinct targets with the same basename no +longer share the same venv output directory. diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 04a43be917..4db12ceb77 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -306,6 +306,9 @@ def _create_executable( else: base_executable_name = executable.basename + # Venv outputs are package-relative, so targets like foo/tool and bar/tool + # need the full label name to avoid both declaring _tool.venv. + venv_output_prefix = ctx.label.name.replace("/", "_") venv = None # The check for stage2_bootstrap_template is to support legacy @@ -318,7 +321,7 @@ def _create_executable( ): venv = _create_venv( ctx, - output_prefix = base_executable_name, + output_prefix = venv_output_prefix, imports = imports, runtime_details = runtime_details, add_runfiles_root_to_sys_path = ( diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 7531de4c30..32ad135636 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -519,6 +519,36 @@ def _test_py_runtime_info_provided_impl(env, target): _tests.append(_test_py_runtime_info_provided) +def _test_venv_output_prefix_with_path_separators(name, config): + rt_util.helper_target( + config.rule, + name = name + "/foo/tool", + srcs = ["main.py"], + main = "main.py", + ) + rt_util.helper_target( + config.rule, + name = name + "/bar/tool", + srcs = ["main.py"], + main = "main.py", + ) + analysis_test( + name = name, + impl = _test_venv_output_prefix_with_path_separators_impl, + targets = { + "bar": name + "/bar/tool", + "foo": name + "/foo/tool", + }, + ) + +def _test_venv_output_prefix_with_path_separators_impl(env, targets): + for target in [targets.foo, targets.bar]: + target = env.expect.that_target(target) + venv_name = "_{}.venv/pyvenv.cfg".format(target.meta.format_str("{name}").replace("/", "_")) + target.runfiles().contains_predicate(matching.str_endswith(venv_name)) + +_tests.append(_test_venv_output_prefix_with_path_separators) + def _test_windows_target_with_path_separators(name, config): rt_util.helper_target( config.rule, From 14bd50af7ab738b3474e7a872415cc849eecbca1 Mon Sep 17 00:00:00 2001 From: Gleb Kolobkov Date: Tue, 23 Jun 2026 13:43:15 -0700 Subject: [PATCH 2/3] fix(rules): preserve slash target venv output paths --- python/private/py_executable.bzl | 6 +++--- tests/base_rules/py_executable_base_tests.bzl | 11 +++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 4db12ceb77..11246ec513 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -306,9 +306,9 @@ def _create_executable( else: base_executable_name = executable.basename - # Venv outputs are package-relative, so targets like foo/tool and bar/tool - # need the full label name to avoid both declaring _tool.venv. - venv_output_prefix = ctx.label.name.replace("/", "_") + # Venv outputs are package-relative, so preserve the full target name to + # avoid collisions between targets like foo/tool, bar/tool, and foo_tool. + venv_output_prefix = ctx.label.name venv = None # The check for stage2_bootstrap_template is to support legacy diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 32ad135636..546ed193f8 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -532,19 +532,26 @@ def _test_venv_output_prefix_with_path_separators(name, config): srcs = ["main.py"], main = "main.py", ) + rt_util.helper_target( + config.rule, + name = name + "/foo_tool", + srcs = ["main.py"], + main = "main.py", + ) analysis_test( name = name, impl = _test_venv_output_prefix_with_path_separators_impl, targets = { "bar": name + "/bar/tool", "foo": name + "/foo/tool", + "foo_underscore": name + "/foo_tool", }, ) def _test_venv_output_prefix_with_path_separators_impl(env, targets): - for target in [targets.foo, targets.bar]: + for target in [targets.foo, targets.bar, targets.foo_underscore]: target = env.expect.that_target(target) - venv_name = "_{}.venv/pyvenv.cfg".format(target.meta.format_str("{name}").replace("/", "_")) + venv_name = "_{}.venv/pyvenv.cfg".format(target.meta.format_str("{name}")) target.runfiles().contains_predicate(matching.str_endswith(venv_name)) _tests.append(_test_venv_output_prefix_with_path_separators) From f9df71318330e50cae71b57ee0b826a073a81e16 Mon Sep 17 00:00:00 2001 From: Gleb Kolobkov Date: Tue, 23 Jun 2026 13:55:46 -0700 Subject: [PATCH 3/3] test(rules): assert stable venv runfile path --- tests/base_rules/py_executable_base_tests.bzl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 546ed193f8..f6b3c9bb60 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -551,8 +551,10 @@ def _test_venv_output_prefix_with_path_separators(name, config): def _test_venv_output_prefix_with_path_separators_impl(env, targets): for target in [targets.foo, targets.bar, targets.foo_underscore]: target = env.expect.that_target(target) - venv_name = "_{}.venv/pyvenv.cfg".format(target.meta.format_str("{name}")) - target.runfiles().contains_predicate(matching.str_endswith(venv_name)) + venv_file = "*/_{}.venv/*site-packages/bazel.pth".format( + target.meta.format_str("{name}"), + ) + target.runfiles().contains_predicate(matching.str_matches(venv_file)) _tests.append(_test_venv_output_prefix_with_path_separators)