refactor: change site_packages_symlinks to venv_symlinks (#2939)

This generalizes the ability to populate the venv directory by adding
and additional field,
`kind`, which tells which directory of the venv to populate. A symbolic
constant is used
to indicate which directory so that users don't have to re-derive the
platform and version
specific paths that make up the venv directory names.

This follows the design described by
https://github.com/bazel-contrib/rules_python/issues/2156#issuecomment-2855580026

This also changes it to a depset of structs to make it more forward
compatible. A provider
is used because they're slightly more memory efficient than regular
structs.

Work towards https://github.com/bazel-contrib/rules_python/issues/2156
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9655b90..4a6bdf0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -70,6 +70,8 @@
   `_test` target is deprecated and will be removed in the next major release.
   ([#2794](https://github.com/bazel-contrib/rules_python/issues/2794)
 * (py_wheel) py_wheel always creates zip64-capable wheel zips
+* (providers) (experimental) {obj}`PyInfo.venv_symlinks` replaces
+  `PyInfo.site_packages_symlinks`
 
 {#v0-0-0-fixed}
 ### Fixed
@@ -203,7 +205,7 @@
   please check the {obj}`uv.configure` tag class.
 * Add support for riscv64 linux platform.
 * (toolchains) Add python 3.13.2 and 3.12.9 toolchains
-* (providers) (experimental) {obj}`PyInfo.site_packages_symlinks` field added to
+* (providers) (experimental) `PyInfo.site_packages_symlinks` field added to
   allow specifying links to create within the venv site packages (only
   applicable with {obj}`--bootstrap_impl=script`)
   ([#2156](https://github.com/bazelbuild/rules_python/issues/2156)).
diff --git a/python/features.bzl b/python/features.bzl
index 917bd38..b678a45 100644
--- a/python/features.bzl
+++ b/python/features.bzl
@@ -31,11 +31,11 @@
     :::
     ::::
 
-    ::::{field} py_info_site_packages_symlinks
+    ::::{field} py_info_venv_symlinks
 
-    True if the `PyInfo.site_packages_symlinks` field is available.
+    True if the `PyInfo.venv_symlinks` field is available.
 
-    :::{versionadded} 1.4.0
+    :::{versionadded} VERSION_NEXT_FEATURE
     :::
     ::::
 
@@ -61,7 +61,7 @@
     TYPEDEF = _features_typedef,
     # keep sorted
     precompile = True,
-    py_info_site_packages_symlinks = True,
+    py_info_venv_symlinks = True,
     uses_builtin_rules = not config.enable_pystar,
     version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "",
 )
diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl
index 98aba4e..ad8cba2 100644
--- a/python/private/attributes.bzl
+++ b/python/private/attributes.bzl
@@ -260,7 +260,7 @@
 from dependencies is merged in, which can be relevant depending on the ordering
 mode of depsets that are merged.
 
-* {obj}`PyInfo.site_packages_symlinks` uses topological ordering.
+* {obj}`PyInfo.venv_symlinks` uses topological ordering.
 
 See {obj}`PyInfo` for more information about the ordering of its depsets and
 how its fields are merged.
diff --git a/python/private/common.bzl b/python/private/common.bzl
index a58a9c0..e49dbad 100644
--- a/python/private/common.bzl
+++ b/python/private/common.bzl
@@ -378,7 +378,7 @@
         implicit_pyc_files,
         implicit_pyc_source_files,
         imports,
-        site_packages_symlinks = []):
+        venv_symlinks = []):
     """Create PyInfo provider.
 
     Args:
@@ -396,7 +396,7 @@
         implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files
             that a binary can choose to include.
         imports: depset of strings; the import path values to propagate.
-        site_packages_symlinks: {type}`list[tuple[str, str]]` tuples of
+        venv_symlinks: {type}`list[tuple[str, str]]` tuples of
             `(runfiles_path, site_packages_path)` for symlinks to create
             in the consuming binary's venv site packages.
 
@@ -406,7 +406,7 @@
         necessary for deprecated extra actions support).
     """
     py_info = PyInfoBuilder.new()
-    py_info.site_packages_symlinks.add(site_packages_symlinks)
+    py_info.venv_symlinks.add(venv_symlinks)
     py_info.direct_original_sources.add(original_sources)
     py_info.direct_pyc_files.add(required_pyc_files)
     py_info.direct_pyi_files.add(ctx.files.pyi_srcs)
diff --git a/python/private/flags.bzl b/python/private/flags.bzl
index 40ce63b..710402b 100644
--- a/python/private/flags.bzl
+++ b/python/private/flags.bzl
@@ -154,12 +154,12 @@
     flag_value = ctx.attr.experimental_venvs_site_packages[BuildSettingInfo].value
     return flag_value == VenvsSitePackages.YES
 
-# Decides if libraries try to use a site-packages layout using site_packages_symlinks
+# Decides if libraries try to use a site-packages layout using venv_symlinks
 # buildifier: disable=name-conventions
 VenvsSitePackages = FlagEnum(
-    # Use site_packages_symlinks
+    # Use venv_symlinks
     YES = "yes",
-    # Don't use site_packages_symlinks
+    # Don't use venv_symlinks
     NO = "no",
     is_enabled = _venvs_site_packages_is_enabled,
 )
diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index 24be8dd..7c3e0cb 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -54,7 +54,7 @@
 load(":precompile.bzl", "maybe_precompile")
 load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
 load(":py_executable_info.bzl", "PyExecutableInfo")
-load(":py_info.bzl", "PyInfo")
+load(":py_info.bzl", "PyInfo", "VenvSymlinkKind")
 load(":py_internal.bzl", "py_internal")
 load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo")
 load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
@@ -543,6 +543,7 @@
         VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES
     )
     recreate_venv_at_runtime = False
+    bin_dir = "{}/bin".format(venv)
 
     if not venvs_use_declare_symlink_enabled or not runtime.supports_build_time_venv:
         recreate_venv_at_runtime = True
@@ -556,7 +557,7 @@
         # When the venv symlinks are disabled, the $venv/bin/python3 file isn't
         # needed or used at runtime. However, the zip code uses the interpreter
         # File object to figure out some paths.
-        interpreter = ctx.actions.declare_file("{}/bin/{}".format(venv, py_exe_basename))
+        interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
         ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path))
 
     elif runtime.interpreter:
@@ -568,7 +569,7 @@
         # declare_symlink() is required to ensure that the resulting file
         # in runfiles is always a symlink. An RBE implementation, for example,
         # may choose to write what symlink() points to instead.
-        interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename))
+        interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
 
         interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
         rel_path = relative_path(
@@ -581,7 +582,7 @@
         ctx.actions.symlink(output = interpreter, target_path = rel_path)
     else:
         py_exe_basename = paths.basename(runtime.interpreter_path)
-        interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename))
+        interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
         ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
         interpreter_actual_path = runtime.interpreter_path
 
@@ -618,89 +619,104 @@
         },
         computed_substitutions = computed_subs,
     )
-    site_packages_symlinks = _create_site_packages_symlinks(ctx, site_packages)
+
+    venv_dir_map = {
+        VenvSymlinkKind.BIN: bin_dir,
+        VenvSymlinkKind.LIB: site_packages,
+    }
+    venv_symlinks = _create_venv_symlinks(ctx, venv_dir_map)
 
     return struct(
         interpreter = interpreter,
         recreate_venv_at_runtime = recreate_venv_at_runtime,
         # Runfiles root relative path or absolute path
         interpreter_actual_path = interpreter_actual_path,
-        files_without_interpreter = [pyvenv_cfg, pth, site_init] + site_packages_symlinks,
+        files_without_interpreter = [pyvenv_cfg, pth, site_init] + venv_symlinks,
         # string; venv-relative path to the site-packages directory.
         venv_site_packages = venv_site_packages,
     )
 
-def _create_site_packages_symlinks(ctx, site_packages):
-    """Creates symlinks within site-packages.
+def _create_venv_symlinks(ctx, venv_dir_map):
+    """Creates symlinks within the venv.
 
     Args:
         ctx: current rule ctx
-        site_packages: runfiles-root-relative path to the site-packages directory
+        venv_dir_map: mapping of VenvSymlinkKind constants to the
+            venv path.
 
     Returns:
         {type}`list[File]` list of the File symlink objects created.
     """
 
-    # maps site-package symlink to the runfiles path it should point to
+    # maps venv-relative path to the runfiles path it should point to
     entries = depset(
         # NOTE: Topological ordering is used so that dependencies closer to the
         # binary have precedence in creating their symlinks. This allows the
         # binary a modicum of control over the result.
         order = "topological",
         transitive = [
-            dep[PyInfo].site_packages_symlinks
+            dep[PyInfo].venv_symlinks
             for dep in ctx.attr.deps
             if PyInfo in dep
         ],
     ).to_list()
-    link_map = _build_link_map(entries)
 
-    sp_files = []
-    for sp_dir_path, link_to in link_map.items():
-        sp_link = ctx.actions.declare_symlink(paths.join(site_packages, sp_dir_path))
-        sp_link_rf_path = runfiles_root_path(ctx, sp_link.short_path)
-        rel_path = relative_path(
-            # dirname is necessary because a relative symlink is relative to
-            # the directory the symlink resides within.
-            from_ = paths.dirname(sp_link_rf_path),
-            to = link_to,
-        )
-        ctx.actions.symlink(output = sp_link, target_path = rel_path)
-        sp_files.append(sp_link)
-    return sp_files
+    link_map = _build_link_map(entries)
+    venv_files = []
+    for kind, kind_map in link_map.items():
+        base = venv_dir_map[kind]
+        for venv_path, link_to in kind_map.items():
+            venv_link = ctx.actions.declare_symlink(paths.join(base, venv_path))
+            venv_link_rf_path = runfiles_root_path(ctx, venv_link.short_path)
+            rel_path = relative_path(
+                # dirname is necessary because a relative symlink is relative to
+                # the directory the symlink resides within.
+                from_ = paths.dirname(venv_link_rf_path),
+                to = link_to,
+            )
+            ctx.actions.symlink(output = venv_link, target_path = rel_path)
+            venv_files.append(venv_link)
+
+    return venv_files
 
 def _build_link_map(entries):
+    # dict[str kind, dict[str rel_path, str link_to_path]]
     link_map = {}
-    for link_to_runfiles_path, site_packages_path in entries:
-        if site_packages_path in link_map:
+    for entry in entries:
+        kind = entry.kind
+        kind_map = link_map.setdefault(kind, {})
+        if entry.venv_path in kind_map:
             # We ignore duplicates by design. The dependency closer to the
             # binary gets precedence due to the topological ordering.
             continue
         else:
-            link_map[site_packages_path] = link_to_runfiles_path
+            kind_map[entry.venv_path] = entry.link_to_path
 
     # An empty link_to value means to not create the site package symlink.
     # Because of the topological ordering, this allows binaries to remove
     # entries by having an earlier dependency produce empty link_to values.
-    for sp_dir_path, link_to in link_map.items():
-        if not link_to:
-            link_map.pop(sp_dir_path)
+    for kind, kind_map in link_map.items():
+        for dir_path, link_to in kind_map.items():
+            if not link_to:
+                kind_map.pop(dir_path)
+
+    # dict[str kind, dict[str rel_path, str link_to_path]]
+    keep_link_map = {}
 
     # Remove entries that would be a child path of a created symlink.
     # Earlier entries have precedence to match how exact matches are handled.
-    keep_link_map = {}
-    for _ in range(len(link_map)):
-        if not link_map:
-            break
-        dirname, value = link_map.popitem()
-        keep_link_map[dirname] = value
-
-        prefix = dirname + "/"  # Add slash to prevent /X matching /XY
-        for maybe_suffix in link_map.keys():
-            maybe_suffix += "/"  # Add slash to prevent /X matching /XY
-            if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix):
-                link_map.pop(maybe_suffix)
-
+    for kind, kind_map in link_map.items():
+        keep_kind_map = keep_link_map.setdefault(kind, {})
+        for _ in range(len(kind_map)):
+            if not kind_map:
+                break
+            dirname, value = kind_map.popitem()
+            keep_kind_map[dirname] = value
+            prefix = dirname + "/"  # Add slash to prevent /X matching /XY
+            for maybe_suffix in kind_map.keys():
+                maybe_suffix += "/"  # Add slash to prevent /X matching /XY
+                if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix):
+                    kind_map.pop(maybe_suffix)
     return keep_link_map
 
 def _map_each_identity(v):
diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl
index d175eef..2a2f455 100644
--- a/python/private/py_info.bzl
+++ b/python/private/py_info.bzl
@@ -18,6 +18,64 @@
 load(":reexports.bzl", "BuiltinPyInfo")
 load(":util.bzl", "define_bazel_6_provider")
 
+def _VenvSymlinkKind_typedef():
+    """An enum of types of venv directories.
+
+    :::{field} BIN
+    :type: object
+
+    Indicates to create paths under the directory that has binaries
+    within the venv.
+    :::
+
+    :::{field} LIB
+    :type: object
+
+    Indicates to create paths under the venv's site-packages directory.
+    :::
+
+    :::{field} INCLUDE
+    :type: object
+
+    Indicates to create paths under the venv's include directory.
+    :::
+    """
+
+# buildifier: disable=name-conventions
+VenvSymlinkKind = struct(
+    TYPEDEF = _VenvSymlinkKind_typedef,
+    BIN = "BIN",
+    LIB = "LIB",
+    INCLUDE = "INCLUDE",
+)
+
+# A provider is used for memory efficiency.
+# buildifier: disable=name-conventions
+VenvSymlinkEntry = provider(
+    doc = """
+An entry in `PyInfo.venv_symlinks`
+""",
+    fields = {
+        "kind": """
+:type: str
+
+One of the {obj}`VenvSymlinkKind` values. It represents which directory within
+the venv to create the path under.
+""",
+        "link_to_path": """
+:type: str | None
+
+A runfiles-root relative path that `venv_path` will symlink to. If `None`,
+it means to not create a symlink.
+""",
+        "venv_path": """
+:type: str
+
+A path relative to the `kind` directory within the venv.
+""",
+    },
+)
+
 def _check_arg_type(name, required_type, value):
     """Check that a value is of an expected type."""
     value_type = type(value)
@@ -43,7 +101,7 @@
         transitive_original_sources = depset(),
         direct_pyi_files = depset(),
         transitive_pyi_files = depset(),
-        site_packages_symlinks = depset()):
+        venv_symlinks = depset()):
     _check_arg_type("transitive_sources", "depset", transitive_sources)
 
     # Verify it's postorder compatible, but retain is original ordering.
@@ -71,7 +129,6 @@
         "has_py2_only_sources": has_py2_only_sources,
         "has_py3_only_sources": has_py2_only_sources,
         "imports": imports,
-        "site_packages_symlinks": site_packages_symlinks,
         "transitive_implicit_pyc_files": transitive_implicit_pyc_files,
         "transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files,
         "transitive_original_sources": transitive_original_sources,
@@ -79,6 +136,7 @@
         "transitive_pyi_files": transitive_pyi_files,
         "transitive_sources": transitive_sources,
         "uses_shared_libraries": uses_shared_libraries,
+        "venv_symlinks": venv_symlinks,
     }
 
 PyInfo, _unused_raw_py_info_ctor = define_bazel_6_provider(
@@ -147,34 +205,6 @@
 The order of the depset is not guaranteed and may be changed in the future. It
 is recommended to use `default` order (the default).
 """,
-        "site_packages_symlinks": """
-:type: depset[tuple[str | None, str]]
-
-A depset with `topological` ordering.
-
-Tuples of `(runfiles_path, site_packages_path)`. Where
-* `runfiles_path` is a runfiles-root relative path. It is the path that
-  has the code to make importable. If `None` or empty string, then it means
-  to not create a site packages directory with the `site_packages_path`
-  name.
-* `site_packages_path` is a path relative to the site-packages directory of
-  the venv for whatever creates the venv (typically py_binary). It makes
-  the code in `runfiles_path` available for import. Note that this
-  is created as a "raw" symlink (via `declare_symlink`).
-
-:::{include} /_includes/experimental_api.md
-:::
-
-:::{tip}
-The topological ordering means dependencies earlier and closer to the consumer
-have precedence. This allows e.g. a binary to add dependencies that override
-values from further way dependencies, such as forcing symlinks to point to
-specific paths or preventing symlinks from being created.
-:::
-
-:::{versionadded} 1.4.0
-:::
-""",
         "transitive_implicit_pyc_files": """
 :type: depset[File]
 
@@ -263,6 +293,35 @@
 
 This field is currently unused in Bazel and may go away in the future.
 """,
+        "venv_symlinks": """
+:type: depset[VenvSymlinkEntry]
+
+A depset with `topological` ordering.
+
+
+Tuples of `(runfiles_path, site_packages_path)`. Where
+* `runfiles_path` is a runfiles-root relative path. It is the path that
+  has the code to make importable. If `None` or empty string, then it means
+  to not create a site packages directory with the `site_packages_path`
+  name.
+* `site_packages_path` is a path relative to the site-packages directory of
+  the venv for whatever creates the venv (typically py_binary). It makes
+  the code in `runfiles_path` available for import. Note that this
+  is created as a "raw" symlink (via `declare_symlink`).
+
+:::{include} /_includes/experimental_api.md
+:::
+
+:::{tip}
+The topological ordering means dependencies earlier and closer to the consumer
+have precedence. This allows e.g. a binary to add dependencies that override
+values from further way dependencies, such as forcing symlinks to point to
+specific paths or preventing symlinks from being created.
+:::
+
+:::{versionadded} VERSION_NEXT_FEATURE
+:::
+""",
     },
 )
 
@@ -314,7 +373,7 @@
     :type: DepsetBuilder[File]
     :::
 
-    :::{field} site_packages_symlinks
+    :::{field} venv_symlinks
     :type: DepsetBuilder[tuple[str | None, str]]
 
     NOTE: This depset has `topological` order
@@ -358,7 +417,7 @@
         transitive_pyc_files = builders.DepsetBuilder(),
         transitive_pyi_files = builders.DepsetBuilder(),
         transitive_sources = builders.DepsetBuilder(),
-        site_packages_symlinks = builders.DepsetBuilder(order = "topological"),
+        venv_symlinks = builders.DepsetBuilder(order = "topological"),
     )
     return self
 
@@ -525,7 +584,7 @@
             self.transitive_original_sources.add(info.transitive_original_sources)
             self.transitive_pyc_files.add(info.transitive_pyc_files)
             self.transitive_pyi_files.add(info.transitive_pyi_files)
-            self.site_packages_symlinks.add(info.site_packages_symlinks)
+            self.venv_symlinks.add(info.venv_symlinks)
 
     return self
 
@@ -583,7 +642,7 @@
             transitive_original_sources = self.transitive_original_sources.build(),
             transitive_pyc_files = self.transitive_pyc_files.build(),
             transitive_pyi_files = self.transitive_pyi_files.build(),
-            site_packages_symlinks = self.site_packages_symlinks.build(),
+            venv_symlinks = self.venv_symlinks.build(),
         )
     else:
         kwargs = {}
diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl
index fd9dad9..fabc880 100644
--- a/python/private/py_library.bzl
+++ b/python/private/py_library.bzl
@@ -43,7 +43,7 @@
 load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag", "VenvsSitePackages")
 load(":precompile.bzl", "maybe_precompile")
 load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
-load(":py_info.bzl", "PyInfo")
+load(":py_info.bzl", "PyInfo", "VenvSymlinkEntry", "VenvSymlinkKind")
 load(":py_internal.bzl", "py_internal")
 load(":reexports.bzl", "BuiltinPyInfo")
 load(":rule_builders.bzl", "ruleb")
@@ -90,9 +90,9 @@
 likely lead to conflicts with other targets that contribute to the namespace.
 
 :::{tip}
-This attributes populates {obj}`PyInfo.site_packages_symlinks`, which is
+This attributes populates {obj}`PyInfo.venv_symlinks`, which is
 a topologically ordered depset. This means dependencies closer and earlier
-to a consumer have precedence. See {obj}`PyInfo.site_packages_symlinks` for
+to a consumer have precedence. See {obj}`PyInfo.venv_symlinks` for
 more information.
 :::
 
@@ -155,9 +155,9 @@
     runfiles = runfiles.build(ctx)
 
     imports = []
-    site_packages_symlinks = []
+    venv_symlinks = []
 
-    imports, site_packages_symlinks = _get_imports_and_site_packages_symlinks(ctx, semantics)
+    imports, venv_symlinks = _get_imports_and_venv_symlinks(ctx, semantics)
 
     cc_info = semantics.get_cc_info_for_library(ctx)
     py_info, deps_transitive_sources, builtins_py_info = create_py_info(
@@ -168,7 +168,7 @@
         implicit_pyc_files = implicit_pyc_files,
         implicit_pyc_source_files = implicit_pyc_source_files,
         imports = imports,
-        site_packages_symlinks = site_packages_symlinks,
+        venv_symlinks = venv_symlinks,
     )
 
     # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455
@@ -206,16 +206,16 @@
 :::
 """
 
-def _get_imports_and_site_packages_symlinks(ctx, semantics):
+def _get_imports_and_venv_symlinks(ctx, semantics):
     imports = depset()
-    site_packages_symlinks = depset()
+    venv_symlinks = depset()
     if VenvsSitePackages.is_enabled(ctx):
-        site_packages_symlinks = _get_site_packages_symlinks(ctx)
+        venv_symlinks = _get_venv_symlinks(ctx)
     else:
         imports = collect_imports(ctx, semantics)
-    return imports, site_packages_symlinks
+    return imports, venv_symlinks
 
-def _get_site_packages_symlinks(ctx):
+def _get_venv_symlinks(ctx):
     imports = ctx.attr.imports
     if len(imports) == 0:
         fail("When venvs_site_packages is enabled, exactly one `imports` " +
@@ -253,7 +253,7 @@
 
     repo_runfiles_dirname = None
     dirs_with_init = {}  # dirname -> runfile path
-    site_packages_symlinks = []
+    venv_symlinks = []
     for src in ctx.files.srcs:
         if src.extension not in PYTHON_FILE_EXTENSIONS:
             continue
@@ -271,9 +271,10 @@
 
             # This would be files that do not have directories and we just need to add
             # direct symlinks to them as is:
-            site_packages_symlinks.append((
-                paths.join(repo_runfiles_dirname, site_packages_root, filename),
-                filename,
+            venv_symlinks.append(VenvSymlinkEntry(
+                kind = VenvSymlinkKind.LIB,
+                link_to_path = paths.join(repo_runfiles_dirname, site_packages_root, filename),
+                venv_path = filename,
             ))
 
     # Sort so that we encounter `foo` before `foo/bar`. This ensures we
@@ -291,11 +292,12 @@
             first_level_explicit_packages.append(d)
 
     for dirname in first_level_explicit_packages:
-        site_packages_symlinks.append((
-            paths.join(repo_runfiles_dirname, site_packages_root, dirname),
-            dirname,
+        venv_symlinks.append(VenvSymlinkEntry(
+            kind = VenvSymlinkKind.LIB,
+            link_to_path = paths.join(repo_runfiles_dirname, site_packages_root, dirname),
+            venv_path = dirname,
         ))
-    return site_packages_symlinks
+    return venv_symlinks
 
 def _repo_relative_short_path(short_path):
     # Convert `../+pypi+foo/some/file.py` to `some/file.py`