From 2100e63c14407d5d62c100d33c38101e3103b0e8 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Thu, 9 Jul 2026 15:38:29 -0700 Subject: [PATCH] fix(py_venv): copy the _virtualenv.py shim instead of symlinking it --- py/private/modify_mtree.awk | 37 +++++++++---------------------- py/private/py_venv/venv.bzl | 7 ++++-- py/tests/py-internal-venv/test.py | 5 +++++ 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/py/private/modify_mtree.awk b/py/private/modify_mtree.awk index 092e8f1dd..67366fbf7 100644 --- a/py/private/modify_mtree.awk +++ b/py/private/modify_mtree.awk @@ -3,24 +3,17 @@ # following and inlining the target bytes. # # Forked from @tar.bzl//tar/private:preserve_symlinks.awk and tracking -# https://github.com/bazel-contrib/tar.bzl/pull/115. Two behavioural -# differences remain: +# https://github.com/bazel-contrib/tar.bzl/pull/115. One behavioural +# difference remains: # -# 1. `bazel-out/` vs `external/` strip is exclusive (`if` / `else if`) -# rather than two sequential `sub`s. Without this, paths matching -# both regexes — e.g. `bazel-out//bin/external//...`, -# the canonical shape of a generated wheel file — get -# over-stripped down to `external//...`, miss the -# `symlink_map` lookup, and dangle inside the OCI layer. -# 2. Classified `bazel-out/` or `external/` targets NOT in the -# layer's mtree fall back to `type=file content=...` (bsdtar -# inlines the bytes) instead of being written as a dangling -# `type=link link=external//...`. Exercised by py_venv's -# `_virtualenv.py` symlink into the rules_py source tree, which -# under bzlmod's `external/aspect_rules_py+/...` layout isn't a -# separate tar entry. +# The `bazel-out/` vs `external/` strip is exclusive (`if` / `else if`) +# rather than two sequential `sub`s. Without this, paths matching +# both regexes — e.g. `bazel-out//bin/external//...`, +# the canonical shape of a generated wheel file — get +# over-stripped down to `external//...`, miss the +# `symlink_map` lookup, and dangle inside the OCI layer. # -# Send a follow-up PR to bazel-contrib/tar.bzl with both once #115 +# Send a follow-up PR to bazel-contrib/tar.bzl once #115 # lands so this fork can retire. # # Invoked from `_run_tar_action` in [py_image_layer.bzl](py_image_layer.bzl) @@ -180,16 +173,8 @@ END { linked_to = make_relative_link(mapped_link, field0) } else if (resolved_path ~ /^bazel-out\// || resolved_path ~ /^external\//) { # Classified to a Bazel-tree path but the target row - # isn't in this layer's mtree. Slow path falls back to - # `type=file content=...` so bsdtar inlines the target - # bytes; the hot path has no equivalent (declared - # symlinks whose targets aren't in the layer are a - # config bug) so we emit a dangling `type=link link=...` - # to surface the issue visibly. - if (original_line ~ /type=file/) { - out_lines[++n] = original_line - continue - } + # isn't in this layer's mtree — a config bug. Emit a + # dangling `type=link link=...` to surface it visibly. linked_to = resolved_path } else { # Already a relative path diff --git a/py/private/py_venv/venv.bzl b/py/private/py_venv/venv.bzl index 0ed284e9b..ba00000f8 100644 --- a/py/private/py_venv/venv.bzl +++ b/py/private/py_venv/venv.bzl @@ -928,12 +928,15 @@ def assemble_venv( declared.append(bin_activate) # _virtualenv.py + _virtualenv.pth — distutils shim for pip interop. + # Materialised as a real file, not a symlink into the rules_py source + # tree, so tar/OCI/rsync etc. consumers of the venv can resolve the file. virtualenv_shim_py_out = ctx.actions.declare_file( "{}/_virtualenv.py".format(site_packages_rel), ) - ctx.actions.symlink( + ctx.actions.expand_template( + template = virtualenv_shim_py, output = virtualenv_shim_py_out, - target_file = virtualenv_shim_py, + substitutions = {}, ) declared.append(virtualenv_shim_py_out) diff --git a/py/tests/py-internal-venv/test.py b/py/tests/py-internal-venv/test.py index da3df9c75..e50c985c7 100644 --- a/py/tests/py-internal-venv/test.py +++ b/py/tests/py-internal-venv/test.py @@ -15,6 +15,11 @@ # The virtualenv module should have already been loaded at interpreter startup assert "_virtualenv" in sys.modules +# The shim must be a real file inside the venv, not a symlink resolving into +# the rules_py source tree — symlinked copies dangle outside Bazel (tar/OCI). +_shim = os.path.realpath(sys.modules["_virtualenv"].__file__) +assert _shim.endswith("site-packages/_virtualenv.py"), _shim + # Note that we can't assume that a `.runfiles` tree has been created as CI may # use a different layout.