15
15
"""Runfiles lookup library for Bazel-built Python binaries and tests.
16
16
17
17
See @rules_python//python/runfiles/README.md for usage instructions.
18
+
19
+ :::{versionadded} VERSION_NEXT_FEATURE
20
+ Support for Bazel's `--incompatible_compact_repo_mapping_manifest` flag was added.
21
+ This enables prefix-based repository mappings to reduce memory usage for large
22
+ dependency graphs under bzlmod.
23
+ :::
18
24
"""
25
+ import collections .abc
19
26
import inspect
20
27
import os
21
28
import posixpath
22
29
import sys
30
+ from collections import defaultdict
23
31
from typing import Dict , Optional , Tuple , Union
24
32
25
33
34
+ class _RepositoryMapping :
35
+ """Repository mapping for resolving apparent repository names to canonical ones.
36
+
37
+ Handles both exact mappings and prefix-based mappings introduced by the
38
+ --incompatible_compact_repo_mapping_manifest flag.
39
+ """
40
+
41
+ def __init__ (
42
+ self ,
43
+ exact_mappings : Dict [Tuple [str , str ], str ],
44
+ prefixed_mappings : Dict [Tuple [str , str ], str ],
45
+ ) -> None :
46
+ """Initialize repository mapping with exact and prefixed mappings.
47
+
48
+ Args:
49
+ exact_mappings: Dict mapping (source_canonical, target_apparent) -> target_canonical
50
+ prefixed_mappings: Dict mapping (source_prefix, target_apparent) -> target_canonical
51
+ """
52
+ self ._exact_mappings = exact_mappings
53
+
54
+ # Group prefixed mappings by target_apparent for faster lookups
55
+ self ._grouped_prefixed_mappings = defaultdict (list )
56
+ for (
57
+ prefix_source ,
58
+ target_app ,
59
+ ), target_canonical in prefixed_mappings .items ():
60
+ self ._grouped_prefixed_mappings [target_app ].append (
61
+ (prefix_source , target_canonical )
62
+ )
63
+
64
+ @staticmethod
65
+ def create_from_file (repo_mapping_path : Optional [str ]) -> "_RepositoryMapping" :
66
+ """Create RepositoryMapping from a repository mapping manifest file.
67
+
68
+ Args:
69
+ repo_mapping_path: Path to the repository mapping file, or None if not available
70
+
71
+ Returns:
72
+ RepositoryMapping instance with parsed mappings
73
+ """
74
+ # If the repository mapping file can't be found, that is not an error: We
75
+ # might be running without Bzlmod enabled or there may not be any runfiles.
76
+ # In this case, just apply empty repo mappings.
77
+ if not repo_mapping_path :
78
+ return _RepositoryMapping ({}, {})
79
+
80
+ try :
81
+ with open (repo_mapping_path , "r" , encoding = "utf-8" , newline = "\n " ) as f :
82
+ content = f .read ()
83
+ except FileNotFoundError :
84
+ return _RepositoryMapping ({}, {})
85
+
86
+ exact_mappings = {}
87
+ prefixed_mappings = {}
88
+ for line in content .splitlines ():
89
+ source_canonical , target_apparent , target_canonical = line .split ("," )
90
+ if source_canonical .endswith ("*" ):
91
+ # This is a prefixed mapping - remove the '*' for prefix matching
92
+ prefix = source_canonical [:- 1 ]
93
+ prefixed_mappings [(prefix , target_apparent )] = target_canonical
94
+ else :
95
+ # This is an exact mapping
96
+ exact_mappings [(source_canonical , target_apparent )] = target_canonical
97
+
98
+ return _RepositoryMapping (exact_mappings , prefixed_mappings )
99
+
100
+ def lookup (self , source_repo : Optional [str ], target_apparent : str ) -> Optional [str ]:
101
+ """Look up repository mapping for the given source and target.
102
+
103
+ This handles both exact mappings and prefix-based mappings introduced by the
104
+ --incompatible_compact_repo_mapping_manifest flag. Exact mappings are tried
105
+ first, followed by prefix-based mappings where order matters.
106
+
107
+ Args:
108
+ source_repo: Source canonical repository name
109
+ target_apparent: Target apparent repository name
110
+
111
+ Returns:
112
+ target_canonical repository name, or None if no mapping exists
113
+ """
114
+ if source_repo is None :
115
+ return None
116
+
117
+ key = (source_repo , target_apparent )
118
+
119
+ # Try exact mapping first
120
+ if key in self ._exact_mappings :
121
+ return self ._exact_mappings [key ]
122
+
123
+ # Try prefixed mapping if no exact match found
124
+ if target_apparent in self ._grouped_prefixed_mappings :
125
+ for prefix_source , target_canonical in self ._grouped_prefixed_mappings [
126
+ target_apparent
127
+ ]:
128
+ if source_repo .startswith (prefix_source ):
129
+ return target_canonical
130
+
131
+ # No mapping found
132
+ return None
133
+
134
+ def is_empty (self ) -> bool :
135
+ """Check if this repository mapping is empty (no exact or prefixed mappings).
136
+
137
+ Returns:
138
+ True if there are no mappings, False otherwise
139
+ """
140
+ return len (self ._exact_mappings ) == 0 and len (self ._grouped_prefixed_mappings ) == 0
141
+
142
+
26
143
class _ManifestBased :
27
144
"""`Runfiles` strategy that parses a runfiles-manifest to look up runfiles."""
28
145
@@ -130,7 +247,7 @@ class Runfiles:
130
247
def __init__ (self , strategy : Union [_ManifestBased , _DirectoryBased ]) -> None :
131
248
self ._strategy = strategy
132
249
self ._python_runfiles_root = _FindPythonRunfilesRoot ()
133
- self ._repo_mapping = _ParseRepoMapping (
250
+ self ._repo_mapping = _RepositoryMapping . create_from_file (
134
251
strategy .RlocationChecked ("_repo_mapping" )
135
252
)
136
253
@@ -179,7 +296,7 @@ def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[st
179
296
if os .path .isabs (path ):
180
297
return path
181
298
182
- if source_repo is None and self ._repo_mapping :
299
+ if source_repo is None and not self ._repo_mapping . is_empty () :
183
300
# Look up runfiles using the repository mapping of the caller of the
184
301
# current method. If the repo mapping is empty, determining this
185
302
# name is not necessary.
@@ -188,7 +305,8 @@ def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[st
188
305
# Split off the first path component, which contains the repository
189
306
# name (apparent or canonical).
190
307
target_repo , _ , remainder = path .partition ("/" )
191
- if not remainder or (source_repo , target_repo ) not in self ._repo_mapping :
308
+ target_canonical = self ._repo_mapping .lookup (source_repo , target_repo )
309
+ if not remainder or target_canonical is None :
192
310
# One of the following is the case:
193
311
# - not using Bzlmod, so the repository mapping is empty and
194
312
# apparent and canonical repository names are the same
@@ -202,11 +320,15 @@ def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[st
202
320
source_repo is not None
203
321
), "BUG: if the `source_repo` is None, we should never go past the `if` statement above"
204
322
205
- # target_repo is an apparent repository name. Look up the corresponding
206
- # canonical repository name with respect to the current repository,
207
- # identified by its canonical name.
208
- target_canonical = self ._repo_mapping [(source_repo , target_repo )]
209
- return self ._strategy .RlocationChecked (target_canonical + "/" + remainder )
323
+ # Look up the target repository using the repository mapping
324
+ if target_canonical is not None :
325
+ return self ._strategy .RlocationChecked (
326
+ target_canonical + "/" + remainder
327
+ )
328
+
329
+ # No mapping found - assume target_repo is already canonical or
330
+ # we're not using Bzlmod
331
+ return self ._strategy .RlocationChecked (path )
210
332
211
333
def EnvVars (self ) -> Dict [str , str ]:
212
334
"""Returns environment variables for subprocesses.
@@ -359,30 +481,6 @@ def _FindPythonRunfilesRoot() -> str:
359
481
return root
360
482
361
483
362
- def _ParseRepoMapping (repo_mapping_path : Optional [str ]) -> Dict [Tuple [str , str ], str ]:
363
- """Parses the repository mapping manifest."""
364
- # If the repository mapping file can't be found, that is not an error: We
365
- # might be running without Bzlmod enabled or there may not be any runfiles.
366
- # In this case, just apply an empty repo mapping.
367
- if not repo_mapping_path :
368
- return {}
369
- try :
370
- with open (repo_mapping_path , "r" , encoding = "utf-8" , newline = "\n " ) as f :
371
- content = f .read ()
372
- except FileNotFoundError :
373
- return {}
374
-
375
- repo_mapping = {}
376
- for line in content .split ("\n " ):
377
- if not line :
378
- # Empty line following the last line break
379
- break
380
- current_canonical , target_local , target_canonical = line .split ("," )
381
- repo_mapping [(current_canonical , target_local )] = target_canonical
382
-
383
- return repo_mapping
384
-
385
-
386
484
def CreateManifestBased (manifest_path : str ) -> Runfiles :
387
485
return Runfiles .CreateManifestBased (manifest_path )
388
486
0 commit comments