Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cbc8774

Browse files
fix: venv site packages with pkgutil packages (#3268)
Currently, an error occurs if one packages files are intended to go into a sub-directory of another package's directory. This can happen when pkgutil-style namespace packages are used, which results in multiple distributions wanting to install the same files (pkgutil `__init__.py` files) into the same top-level directories. This eventually results in a Bazel error because Bazel detects that the one output is the prefix of another. To fix, detect when distributions overlap in their paths and merge their files manually. Internally, entries are sorted from shorted venv path to longest, however, that's just an implementation detail. Along the way, give agents better advice for bzl_library targets. Fixes #3204 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 77cf48d commit cbc8774

File tree

19 files changed

+697
-235
lines changed

19 files changed

+697
-235
lines changed

AGENTS.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,16 @@ into the sentence, not verbatim.
2121

2222
### bzl_library targets for bzl source files
2323

24-
* `.bzl` files should have `bzl_library` defined for them.
24+
* A `bzl_library` target should be defined for every `.bzl` file outside
25+
of the `tests/` directory.
2526
* They should have a single `srcs` file and be named after the file with `_bzl`
2627
appended.
27-
* Their deps should be based on the `load()` statements in the source file.
28+
* Their deps should be based on the `load()` statements in the source file
29+
and refer to the `bzl_library` target containing the loaded file.
30+
* For files in rules_python: replace `.bzl` with `_bzl`.
31+
e.g. given `load("//foo:bar.bzl", ...)`, the target is `//foo:bar_bzl`.
32+
* For files outside rules_python: remove the `.bzl` suffix. e.g. given
33+
`load("@foo//foo:bar.bzl", ...)`, the target is `@foo//foo:bar`.
2834
* `bzl_library()` targets should be kept in alphabetical order by name.
2935

3036
Example:

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ END_UNRELEASED_TEMPLATE
8686
{attr}`pip.defaults.whl_platform_tags` attribute to configure that. If
8787
`musllinux_*_x86_64` is specified, we will chose the lowest available
8888
wheel version. Fixes [#3250](https://github.com/bazel-contrib/rules_python/issues/3250).
89+
* (venvs) {obj}`--vens_site_packages=yes` no longer errors when packages with
90+
overlapping files or directories are used together.
91+
([#3204](https://github.com/bazel-contrib/rules_python/issues/3204)).
8992

9093
{#v0-0-0-added}
9194
### Added

python/private/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ bzl_library(
419419
":rules_cc_srcs_bzl",
420420
":toolchain_types_bzl",
421421
":transition_labels_bzl",
422+
":venv_runfiles_bzl",
422423
"@bazel_skylib//lib:dicts",
423424
"@bazel_skylib//lib:paths",
424425
"@bazel_skylib//lib:structs",
@@ -468,6 +469,7 @@ bzl_library(
468469
":py_internal_bzl",
469470
":rule_builders_bzl",
470471
":toolchain_types_bzl",
472+
":venv_runfiles_bzl",
471473
":version_bzl",
472474
"@bazel_skylib//lib:dicts",
473475
"@bazel_skylib//rules:common_settings",
@@ -727,6 +729,16 @@ bzl_library(
727729
],
728730
)
729731

732+
bzl_library(
733+
name = "venv_runfiles_bzl",
734+
srcs = ["venv_runfiles.bzl"],
735+
deps = [
736+
":common_bzl",
737+
":py_info.bzl",
738+
"@bazel_skylib//lib:paths",
739+
],
740+
)
741+
730742
# Needed to define bzl_library targets for docgen. (We don't define the
731743
# bzl_library target here because it'd give our users a transitive dependency
732744
# on Skylib.)

python/private/common.bzl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,9 @@ _BOOL_TYPE = type(True)
495495
def is_bool(v):
496496
return type(v) == _BOOL_TYPE
497497

498+
def is_file(v):
499+
return type(v) == "File"
500+
498501
def target_platform_has_any_constraint(ctx, constraints):
499502
"""Check if target platform has any of a list of constraints.
500503
@@ -511,6 +514,37 @@ def target_platform_has_any_constraint(ctx, constraints):
511514
return True
512515
return False
513516

517+
def relative_path(from_, to):
518+
"""Compute a relative path from one path to another.
519+
520+
Args:
521+
from_: {type}`str` the starting directory. Note that it should be
522+
a directory because relative-symlinks are relative to the
523+
directory the symlink resides in.
524+
to: {type}`str` the path that `from_` wants to point to
525+
526+
Returns:
527+
{type}`str` a relative path
528+
"""
529+
from_parts = from_.split("/")
530+
to_parts = to.split("/")
531+
532+
# Strip common leading parts from both paths
533+
n = min(len(from_parts), len(to_parts))
534+
for _ in range(n):
535+
if from_parts[0] == to_parts[0]:
536+
from_parts.pop(0)
537+
to_parts.pop(0)
538+
else:
539+
break
540+
541+
# Impossible to compute a relative path without knowing what ".." is
542+
if from_parts and from_parts[0] == "..":
543+
fail("cannot compute relative path from '%s' to '%s'", from_, to)
544+
545+
parts = ([".."] * len(from_parts)) + to_parts
546+
return paths.join(*parts)
547+
514548
def runfiles_root_path(ctx, short_path):
515549
"""Compute a runfiles-root relative path from `File.short_path`
516550

python/private/py_executable.bzl

Lines changed: 4 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ load(
4848
"filter_to_py_srcs",
4949
"get_imports",
5050
"is_bool",
51+
"relative_path",
5152
"runfiles_root_path",
5253
"target_platform_has_any_constraint",
5354
)
@@ -63,6 +64,7 @@ load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
6364
load(":rule_builders.bzl", "ruleb")
6465
load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE")
6566
load(":transition_labels.bzl", "TRANSITION_LABELS")
67+
load(":venv_runfiles.bzl", "create_venv_app_files")
6668

6769
_py_builtins = py_internal
6870
_EXTERNAL_PATH_PREFIX = "external"
@@ -499,37 +501,6 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
499501
)
500502
return output
501503

502-
def relative_path(from_, to):
503-
"""Compute a relative path from one path to another.
504-
505-
Args:
506-
from_: {type}`str` the starting directory. Note that it should be
507-
a directory because relative-symlinks are relative to the
508-
directory the symlink resides in.
509-
to: {type}`str` the path that `from_` wants to point to
510-
511-
Returns:
512-
{type}`str` a relative path
513-
"""
514-
from_parts = from_.split("/")
515-
to_parts = to.split("/")
516-
517-
# Strip common leading parts from both paths
518-
n = min(len(from_parts), len(to_parts))
519-
for _ in range(n):
520-
if from_parts[0] == to_parts[0]:
521-
from_parts.pop(0)
522-
to_parts.pop(0)
523-
else:
524-
break
525-
526-
# Impossible to compute a relative path without knowing what ".." is
527-
if from_parts and from_parts[0] == "..":
528-
fail("cannot compute relative path from '%s' to '%s'", from_, to)
529-
530-
parts = ([".."] * len(from_parts)) + to_parts
531-
return paths.join(*parts)
532-
533504
# Create a venv the executable can use.
534505
# For venv details and the venv startup process, see:
535506
# * https://docs.python.org/3/library/venv.html
@@ -636,9 +607,9 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
636607
VenvSymlinkKind.BIN: bin_dir,
637608
VenvSymlinkKind.LIB: site_packages,
638609
}
639-
venv_symlinks = _create_venv_symlinks(ctx, venv_dir_map)
610+
venv_app_files = create_venv_app_files(ctx, ctx.attr.deps, venv_dir_map)
640611

641-
files_without_interpreter = [pth, site_init] + venv_symlinks
612+
files_without_interpreter = [pth, site_init] + venv_app_files
642613
if pyvenv_cfg:
643614
files_without_interpreter.append(pyvenv_cfg)
644615

@@ -663,94 +634,6 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
663634
),
664635
)
665636

666-
def _create_venv_symlinks(ctx, venv_dir_map):
667-
"""Creates symlinks within the venv.
668-
669-
Args:
670-
ctx: current rule ctx
671-
venv_dir_map: mapping of VenvSymlinkKind constants to the
672-
venv path.
673-
674-
Returns:
675-
{type}`list[File]` list of the File symlink objects created.
676-
"""
677-
678-
# maps venv-relative path to the runfiles path it should point to
679-
entries = depset(
680-
transitive = [
681-
dep[PyInfo].venv_symlinks
682-
for dep in ctx.attr.deps
683-
if PyInfo in dep
684-
],
685-
).to_list()
686-
687-
link_map = _build_link_map(entries)
688-
venv_files = []
689-
for kind, kind_map in link_map.items():
690-
base = venv_dir_map[kind]
691-
for venv_path, link_to in kind_map.items():
692-
venv_link = ctx.actions.declare_symlink(paths.join(base, venv_path))
693-
venv_link_rf_path = runfiles_root_path(ctx, venv_link.short_path)
694-
rel_path = relative_path(
695-
# dirname is necessary because a relative symlink is relative to
696-
# the directory the symlink resides within.
697-
from_ = paths.dirname(venv_link_rf_path),
698-
to = link_to,
699-
)
700-
ctx.actions.symlink(output = venv_link, target_path = rel_path)
701-
venv_files.append(venv_link)
702-
703-
return venv_files
704-
705-
def _build_link_map(entries):
706-
# dict[str package, dict[str kind, dict[str rel_path, str link_to_path]]]
707-
pkg_link_map = {}
708-
709-
# dict[str package, str version]
710-
version_by_pkg = {}
711-
712-
for entry in entries:
713-
link_map = pkg_link_map.setdefault(entry.package, {})
714-
kind_map = link_map.setdefault(entry.kind, {})
715-
716-
if version_by_pkg.setdefault(entry.package, entry.version) != entry.version:
717-
# We ignore duplicates by design.
718-
continue
719-
elif entry.venv_path in kind_map:
720-
# We ignore duplicates by design.
721-
continue
722-
else:
723-
kind_map[entry.venv_path] = entry.link_to_path
724-
725-
# An empty link_to value means to not create the site package symlink. Because of the
726-
# ordering, this allows binaries to remove entries by having an earlier dependency produce
727-
# empty link_to values.
728-
for link_map in pkg_link_map.values():
729-
for kind, kind_map in link_map.items():
730-
for dir_path, link_to in kind_map.items():
731-
if not link_to:
732-
kind_map.pop(dir_path)
733-
734-
# dict[str kind, dict[str rel_path, str link_to_path]]
735-
keep_link_map = {}
736-
737-
# Remove entries that would be a child path of a created symlink.
738-
# Earlier entries have precedence to match how exact matches are handled.
739-
for link_map in pkg_link_map.values():
740-
for kind, kind_map in link_map.items():
741-
keep_kind_map = keep_link_map.setdefault(kind, {})
742-
for _ in range(len(kind_map)):
743-
if not kind_map:
744-
break
745-
dirname, value = kind_map.popitem()
746-
keep_kind_map[dirname] = value
747-
prefix = dirname + "/" # Add slash to prevent /X matching /XY
748-
for maybe_suffix in kind_map.keys():
749-
maybe_suffix += "/" # Add slash to prevent /X matching /XY
750-
if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix):
751-
kind_map.pop(maybe_suffix)
752-
return keep_link_map
753-
754637
def _map_each_identity(v):
755638
return v
756639

python/private/py_info.bzl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ VenvSymlinkEntry = provider(
5656
An entry in `PyInfo.venv_symlinks`
5757
""",
5858
fields = {
59+
"files": """
60+
:type: depset[File]
61+
62+
Files under `link_to_path`.
63+
64+
This is only used when multiple targets have overlapping `venv_path` paths. e.g.
65+
if one adds files to `venv_path=a/` and another adds files to `venv_path=a/b/`.
66+
""",
5967
"kind": """
6068
:type: str
6169

0 commit comments

Comments
 (0)