Skip to content

Commit df77fa5

Browse files
committed
fix: site packages with pkgutil
1 parent 029a4dc commit df77fa5

File tree

15 files changed

+365
-123
lines changed

15 files changed

+365
-123
lines changed

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
)
@@ -67,6 +68,7 @@ load(
6768
TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE",
6869
)
6970
load(":transition_labels.bzl", "TRANSITION_LABELS")
71+
load(":venv_runfiles.bzl", "create_venv_app_files")
7072

7173
_py_builtins = py_internal
7274
_EXTERNAL_PATH_PREFIX = "external"
@@ -504,37 +506,6 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
504506
)
505507
return output
506508

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

646-
files_without_interpreter = [pth, site_init] + venv_symlinks
617+
files_without_interpreter = [pth, site_init] + venv_app_files
647618
if pyvenv_cfg:
648619
files_without_interpreter.append(pyvenv_cfg)
649620

@@ -668,94 +639,6 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
668639
),
669640
)
670641

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

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

python/private/py_library.bzl

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,12 @@ def _get_venv_symlinks(ctx, package, version_str):
277277

278278
dir_symlinks = {} # dirname -> runfile path
279279
venv_symlinks = []
280-
for src in ctx.files.srcs + ctx.files.data + ctx.files.pyi_srcs:
280+
all_files = sorted(
281+
ctx.files.srcs + ctx.files.data + ctx.files.pyi_srcs,
282+
key = lambda f: f.short_path,
283+
)
284+
285+
for src in all_files:
281286
path = _repo_relative_short_path(src.short_path)
282287
if not path.startswith(site_packages_root):
283288
continue
@@ -309,6 +314,7 @@ def _get_venv_symlinks(ctx, package, version_str):
309314
package = package,
310315
version = version_str,
311316
venv_path = filename,
317+
files = depset([src]),
312318
)
313319
venv_symlinks.append(entry)
314320

@@ -328,12 +334,18 @@ def _get_venv_symlinks(ctx, package, version_str):
328334

329335
for dirname in first_level_explicit_packages:
330336
prefix = dir_symlinks[dirname]
337+
link_to_path = paths.join(prefix, site_packages_root, dirname)
331338
entry = VenvSymlinkEntry(
332339
kind = VenvSymlinkKind.LIB,
333-
link_to_path = paths.join(prefix, site_packages_root, dirname),
340+
link_to_path = link_to_path,
334341
package = package,
335342
version = version_str,
336343
venv_path = dirname,
344+
files = depset([
345+
f
346+
for f in all_files
347+
if runfiles_root_path(ctx, f.short_path).startswith(link_to_path + "/")
348+
]),
337349
)
338350
venv_symlinks.append(entry)
339351

0 commit comments

Comments
 (0)