|
| 1 | +""" |
| 2 | +Copyright (c) 2025, salesforce.com, inc. |
| 3 | +All rights reserved. |
| 4 | +SPDX-License-Identifier: BSD-3-Clause |
| 5 | +For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause |
| 6 | +
|
| 7 | +This module has abstractions for Bazel labels. |
| 8 | +""" |
| 9 | + |
| 10 | + |
| 11 | +import os |
| 12 | + |
| 13 | + |
| 14 | +_BUILD_FILE_NAMES = ("BUILD", "BUILD.bazel") |
| 15 | + |
| 16 | + |
| 17 | +def for_package(root_dir, package_path): |
| 18 | + """ |
| 19 | + Returns a label instance for the Bazel package at the given relative path, |
| 20 | + rooted at the given root_dir. Returns None if no build file exists at that |
| 21 | + location. |
| 22 | + """ |
| 23 | + for fname in _BUILD_FILE_NAMES: |
| 24 | + rel_path = os.path.join(package_path, fname) |
| 25 | + abs_path = os.path.join(root_dir, rel_path) |
| 26 | + if os.path.isfile(abs_path): |
| 27 | + return Label(rel_path) |
| 28 | + return None |
| 29 | + |
| 30 | + |
| 31 | +def find_packages(root_dir, package_path=""): |
| 32 | + """ |
| 33 | + Walks the directory tree, starting at root dir, and returns a list of |
| 34 | + Label instances for all Bazel packages that exist under the given root_dir. |
| 35 | +
|
| 36 | + If package_path is specified, the search starts at that location. |
| 37 | + """ |
| 38 | + labels = [] |
| 39 | + for path, dirs, files in os.walk(os.path.join(root_dir, package_path)): |
| 40 | + for fname in files: |
| 41 | + if fname in _BUILD_FILE_NAMES: |
| 42 | + rel_path = os.path.join(os.path.relpath(path, root_dir), fname) |
| 43 | + if rel_path.startswith("./"): |
| 44 | + # build file at root dir, remove "./" so that the package |
| 45 | + # of the Label is empty |
| 46 | + rel_path = rel_path[2:] |
| 47 | + labels.append(Label(rel_path)) |
| 48 | + return labels |
| 49 | + |
| 50 | + |
| 51 | +class Label(object): |
| 52 | + """ |
| 53 | + Represents a Bazel Label. |
| 54 | + """ |
| 55 | + |
| 56 | + def __init__(self, name): |
| 57 | + """ |
| 58 | + Initializes a Label with the given name, a string. name represents |
| 59 | + a path-like structure with an optional target [path:target]. |
| 60 | +
|
| 61 | + If the last path segment is a build file (/BUILD or /BUILD.bazel), |
| 62 | + it is removed from the path. |
| 63 | + |
| 64 | + """ |
| 65 | + assert name is not None |
| 66 | + name = name.strip() |
| 67 | + if name.endswith("/"): |
| 68 | + name = name[:-1] |
| 69 | + fname = os.path.basename(name) |
| 70 | + if fname in ("BUILD", "BUILD.bazel"): |
| 71 | + name = os.path.dirname(name) |
| 72 | + self._build_file_name = fname |
| 73 | + else: |
| 74 | + self._build_file_name = None |
| 75 | + self._name = name |
| 76 | + |
| 77 | + @property |
| 78 | + def name(self): |
| 79 | + """ |
| 80 | + The name this instance was initialized with. |
| 81 | + """ |
| 82 | + return self._name |
| 83 | + |
| 84 | + @property |
| 85 | + def package(self): |
| 86 | + """ |
| 87 | + The Bazel Package of this label. |
| 88 | + For example, for "//a/b/c:foo", return "//a/b/c" |
| 89 | + """ |
| 90 | + i = self._name.find(":") |
| 91 | + if i == -1: |
| 92 | + if self._name.endswith("..."): |
| 93 | + return self._name[:-3] |
| 94 | + return self._name |
| 95 | + return self._name[0:i] |
| 96 | + |
| 97 | + @property |
| 98 | + def package_path(self): |
| 99 | + """ |
| 100 | + Returns the Package of this label as a valid relative path. |
| 101 | + """ |
| 102 | + p = self.package |
| 103 | + if p.startswith("//"): |
| 104 | + p = p[2:] |
| 105 | + if p.endswith("/"): |
| 106 | + p = p[:-1] |
| 107 | + return p |
| 108 | + |
| 109 | + @property |
| 110 | + def target(self): |
| 111 | + """ |
| 112 | + The Bazel Target of this label. |
| 113 | + For example, for "//a/b/c:foo", return "foo" |
| 114 | + """ |
| 115 | + i = self._name.find(":") |
| 116 | + if i == -1: |
| 117 | + return os.path.basename(self._name) |
| 118 | + return self._name[i+1:] |
| 119 | + |
| 120 | + @property |
| 121 | + def target_name(self): |
| 122 | + """ |
| 123 | + An alias for the "target" property. |
| 124 | + """ |
| 125 | + return self.target |
| 126 | + |
| 127 | + @property |
| 128 | + def is_default_target(self): |
| 129 | + """ |
| 130 | + Returns True if this label refers to the default target in the package, |
| 131 | + ie the target that has the same name as the directory the BUILD file |
| 132 | + lives in. |
| 133 | + """ |
| 134 | + package = self.package |
| 135 | + target = self.target |
| 136 | + if package is None: |
| 137 | + return False |
| 138 | + if target is None: |
| 139 | + return True |
| 140 | + return os.path.basename(package) == target |
| 141 | + |
| 142 | + @property |
| 143 | + def is_root_target(self): |
| 144 | + """ |
| 145 | + Returns True if this label's target is defined in the root BUILD |
| 146 | + file, ie if the label has this pattern "//:" |
| 147 | + """ |
| 148 | + return "//:" in self._name |
| 149 | + |
| 150 | + @property |
| 151 | + def fqname(self): |
| 152 | + """ |
| 153 | + The name of this label with a default repo prefix, iff the |
| 154 | + initial name did not specify such a prefix and this is not a src ref. |
| 155 | + """ |
| 156 | + if self.is_source_ref: |
| 157 | + return self._name |
| 158 | + if self.has_repo_prefix: |
| 159 | + return self._name |
| 160 | + else: |
| 161 | + # the default prefix we use for names without repo prefix: |
| 162 | + # if name is foo, fqname will be @maven//:foo |
| 163 | + # "maven" doesn't really make sense to use anymore, but it isn't |
| 164 | + # clear what to use instead - probably defaulting the repo doesn't |
| 165 | + # make sense |
| 166 | + default_repo = "maven" |
| 167 | + return self.prefix_with(default_repo).name |
| 168 | + |
| 169 | + @property |
| 170 | + def simple_name(self): |
| 171 | + """ |
| 172 | + The name of this label without the remote repo prefix. |
| 173 | + If this label does not have a remote repo prefix, returns just |
| 174 | + its name. |
| 175 | + """ |
| 176 | + if self.is_source_ref: |
| 177 | + return self._name |
| 178 | + if self.has_repo_prefix: |
| 179 | + prefix = self.repo_prefix |
| 180 | + return self._name[len(prefix)+4:] # 4 = additional chars @//: |
| 181 | + else: |
| 182 | + return self._name |
| 183 | + |
| 184 | + @property |
| 185 | + def is_private(self): |
| 186 | + """ |
| 187 | + Returns True if this label refers to a private target (starts with ":") |
| 188 | + """ |
| 189 | + return self._name.startswith(":") |
| 190 | + |
| 191 | + @property |
| 192 | + def has_repo_prefix(self): |
| 193 | + """ |
| 194 | + Whether this label name has a remote repo prefix. |
| 195 | + """ |
| 196 | + return self.repo_prefix is not None |
| 197 | + |
| 198 | + @property |
| 199 | + def repo_prefix(self): |
| 200 | + """ |
| 201 | + The remote repo prefix, or workspace name of this label; None if this |
| 202 | + label name doesn't have one. |
| 203 | +
|
| 204 | + For example, for a label like "@pomgen//maven", returns "pomgen". |
| 205 | + """ |
| 206 | + if self._name.startswith("@"): |
| 207 | + i = self._name.find("//") |
| 208 | + if i != -1: |
| 209 | + return self._name[1:i] |
| 210 | + return None |
| 211 | + |
| 212 | + @property |
| 213 | + def is_source_ref(self): |
| 214 | + """ |
| 215 | + True if this name is a reference to source in the same repository. |
| 216 | + """ |
| 217 | + return self._name.startswith("//") |
| 218 | + |
| 219 | + @property |
| 220 | + def has_file_extension(self): |
| 221 | + ext = os.path.splitext(self._name)[1] |
| 222 | + return ext in (".jar", ".proto", ".h", ".c", ".cc", ".cpp", ".m", ".py", ".pyc", ".java", ".go") |
| 223 | + |
| 224 | + @property |
| 225 | + def has_extension_suffix(self): |
| 226 | + return self._name.endswith("_extension") |
| 227 | + |
| 228 | + @property |
| 229 | + def is_sources_artifact(self): |
| 230 | + return "_jar_sources" in self._name |
| 231 | + |
| 232 | + @property |
| 233 | + def build_file_path(self): |
| 234 | + """ |
| 235 | + The path to the build file of this package, if this Label instance was |
| 236 | + created with a path that pointed to a build file. |
| 237 | + None if this Label instance does not know about the build file it was |
| 238 | + created for. |
| 239 | + """ |
| 240 | + if self._build_file_name is None: |
| 241 | + return None |
| 242 | + return os.path.join(self.package_path, self._build_file_name) |
| 243 | + |
| 244 | + def prefix_with(self, repo_prefix): |
| 245 | + """ |
| 246 | + Returns a new Label instance that is qualified with the |
| 247 | + specified repo_prefix. This method asserts that this instance is not |
| 248 | + already fully qualified. |
| 249 | + """ |
| 250 | + assert not self.has_repo_prefix, "This label already has a repo prefix: %s" % self._name |
| 251 | + return Label("@%s//:%s" % (repo_prefix, self._name)) |
| 252 | + |
| 253 | + def with_target(self, target): |
| 254 | + """ |
| 255 | + Returns a new Label instance that has the specified target. |
| 256 | + """ |
| 257 | + return Label("%s:%s" % (self.package, target)) |
| 258 | + |
| 259 | + def as_wildcard_label(self, wildcard): |
| 260 | + if wildcard == "...": |
| 261 | + return Label("%s/%s" % (self.package, wildcard)) |
| 262 | + else: |
| 263 | + return Label("%s:%s" % (self.package, wildcard)) |
| 264 | + |
| 265 | + def as_alternate_default_target_syntax(self): |
| 266 | + """ |
| 267 | + Labels may omit the target if they refer to the default target, or they |
| 268 | + may not omit it. If this Label instance refers to the default target, |
| 269 | + this method returns the other syntax. |
| 270 | + So: |
| 271 | + Given this Label instance is: //a/b/c, returns //a/b/c:c |
| 272 | + Or, given this Label instance is //a/b/c:c, returns //a/b/c |
| 273 | + """ |
| 274 | + assert self.is_default_target, "label must refer to the default target" |
| 275 | + if ":" in self.name: |
| 276 | + return Label(self.package) |
| 277 | + else: |
| 278 | + return Label("%s:%s" % (self.package, self.target)) |
| 279 | + |
| 280 | + def __hash__(self): |
| 281 | + return hash((self.package_path, self.target)) |
| 282 | + |
| 283 | + def __eq__(self, other): |
| 284 | + if other is None: |
| 285 | + return False |
| 286 | + return self.package_path == other.package_path and self.target == other.target |
| 287 | + |
| 288 | + def __ne__(self, other): |
| 289 | + return not self == other |
| 290 | + |
| 291 | + def __len__(self): |
| 292 | + return len(self._name) |
| 293 | + |
| 294 | + def __repr__(self): |
| 295 | + return self._name |
| 296 | + |
| 297 | + __str__ = __repr__ |
0 commit comments