1 use std::fmt::{self, Display};
2 use std::path::Path;
3 use std::str::FromStr;
4
5 use anyhow::{anyhow, bail, Context, Result};
6 use camino::Utf8Path;
7 use once_cell::sync::OnceCell;
8 use regex::Regex;
9 use serde::de::Visitor;
10 use serde::{Deserialize, Serialize, Serializer};
11
12 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
13 pub(crate) enum Label {
14 Relative {
15 target: String,
16 },
17 Absolute {
18 repository: Repository,
19 package: String,
20 target: String,
21 },
22 }
23
24 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
25 pub(crate) enum Repository {
26 Canonical(String), // stringifies to `@@self.0` where `self.0` may be empty
27 Explicit(String), // stringifies to `@self.0` where `self.0` may be empty
28 Local, // stringifies to the empty string
29 }
30
31 impl Label {
32 #[cfg(test)]
is_absolute(&self) -> bool33 pub(crate) fn is_absolute(&self) -> bool {
34 match self {
35 Label::Relative { .. } => false,
36 Label::Absolute { .. } => true,
37 }
38 }
39
40 #[cfg(test)]
repository(&self) -> Option<&Repository>41 pub(crate) fn repository(&self) -> Option<&Repository> {
42 match self {
43 Label::Relative { .. } => None,
44 Label::Absolute { repository, .. } => Some(repository),
45 }
46 }
47
package(&self) -> Option<&str>48 pub(crate) fn package(&self) -> Option<&str> {
49 match self {
50 Label::Relative { .. } => None,
51 Label::Absolute { package, .. } => Some(package.as_str()),
52 }
53 }
54
target(&self) -> &str55 pub(crate) fn target(&self) -> &str {
56 match self {
57 Label::Relative { target } => target.as_str(),
58 Label::Absolute { target, .. } => target.as_str(),
59 }
60 }
61 }
62
63 impl FromStr for Label {
64 type Err = anyhow::Error;
65
from_str(s: &str) -> Result<Self, Self::Err>66 fn from_str(s: &str) -> Result<Self, Self::Err> {
67 static RE: OnceCell<Regex> = OnceCell::new();
68 let re = RE.get_or_try_init(|| {
69 Regex::new(r"^(@@?[\w\d\-_\.~]*)?(//)?([\w\d\-_\./+]+)?(:([\+\w\d\-_\./]+))?$")
70 });
71
72 let cap = re?
73 .captures(s)
74 .with_context(|| format!("Failed to parse label from string: {s}"))?;
75
76 let (repository, is_absolute) = match (cap.get(1), cap.get(2).is_some()) {
77 (Some(repository), is_absolute) => match *repository.as_str().as_bytes() {
78 [b'@', b'@', ..] => (
79 Some(Repository::Canonical(repository.as_str()[2..].to_owned())),
80 is_absolute,
81 ),
82 [b'@', ..] => (
83 Some(Repository::Explicit(repository.as_str()[1..].to_owned())),
84 is_absolute,
85 ),
86 _ => bail!("Invalid Label: {}", s),
87 },
88 (None, true) => (Some(Repository::Local), true),
89 (None, false) => (None, false),
90 };
91
92 let package = cap.get(3).map(|package| package.as_str().to_owned());
93
94 let target = cap.get(5).map(|target| target.as_str().to_owned());
95
96 match repository {
97 None => match (package, target) {
98 // Relative
99 (None, Some(target)) => Ok(Label::Relative { target }),
100
101 // Relative (Implicit Target which regex identifies as Package)
102 (Some(package), None) => Ok(Label::Relative { target: package }),
103
104 // Invalid (Empty)
105 (None, None) => bail!("Invalid Label: {}", s),
106
107 // Invalid (Relative Package + Target)
108 (Some(_), Some(_)) => bail!("Invalid Label: {}", s),
109 },
110 Some(repository) => match (is_absolute, package, target) {
111 // Absolute (Full)
112 (true, Some(package), Some(target)) => Ok(Label::Absolute {
113 repository,
114 package,
115 target,
116 }),
117
118 // Absolute (Repository)
119 (_, None, None) => match &repository {
120 Repository::Canonical(target) | Repository::Explicit(target) => {
121 let target = match target.is_empty() {
122 false => target.clone(),
123 true => bail!("Invalid Label: {}", s),
124 };
125 Ok(Label::Absolute {
126 repository,
127 package: String::new(),
128 target,
129 })
130 }
131 Repository::Local => bail!("Invalid Label: {}", s),
132 },
133
134 // Absolute (Package)
135 (true, Some(package), None) => {
136 let target = Utf8Path::new(&package)
137 .file_name()
138 .with_context(|| format!("Invalid Label: {}", s))?
139 .to_owned();
140 Ok(Label::Absolute {
141 repository,
142 package,
143 target,
144 })
145 }
146
147 // Absolute (Target)
148 (true, None, Some(target)) => Ok(Label::Absolute {
149 repository,
150 package: String::new(),
151 target,
152 }),
153
154 // Invalid (Relative Repository + Package + Target)
155 (false, Some(_), Some(_)) => bail!("Invalid Label: {}", s),
156
157 // Invalid (Relative Repository + Package)
158 (false, Some(_), None) => bail!("Invalid Label: {}", s),
159
160 // Invalid (Relative Repository + Target)
161 (false, None, Some(_)) => bail!("Invalid Label: {}", s),
162 },
163 }
164 }
165 }
166
167 impl Display for Label {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 match self {
170 Label::Relative { target } => write!(f, ":{}", target),
171 Label::Absolute {
172 repository,
173 package,
174 target,
175 } => match repository {
176 Repository::Canonical(repository) => {
177 write!(f, "@@{repository}//{package}:{target}")
178 }
179 Repository::Explicit(repository) => {
180 write!(f, "@{repository}//{package}:{target}")
181 }
182 Repository::Local => write!(f, "//{package}:{target}"),
183 },
184 }
185 }
186 }
187
188 impl Label {
189 /// Generates a label appropriate for the passed Path by walking the filesystem to identify its
190 /// workspace and package.
from_absolute_path(p: &Path) -> Result<Self, anyhow::Error>191 pub(crate) fn from_absolute_path(p: &Path) -> Result<Self, anyhow::Error> {
192 let mut workspace_root = None;
193 let mut package_root = None;
194 for ancestor in p.ancestors().skip(1) {
195 if package_root.is_none()
196 && (ancestor.join("BUILD").exists() || ancestor.join("BUILD.bazel").exists())
197 {
198 package_root = Some(ancestor);
199 }
200 if workspace_root.is_none()
201 && (ancestor.join("WORKSPACE").exists()
202 || ancestor.join("WORKSPACE.bazel").exists()
203 || ancestor.join("MODULE.bazel").exists())
204 {
205 workspace_root = Some(ancestor);
206 break;
207 }
208 }
209 match (workspace_root, package_root) {
210 (Some(workspace_root), Some(package_root)) => {
211 // These unwraps are safe by construction of the ancestors and prefix calls which set up these paths.
212 let target = p.strip_prefix(package_root).unwrap();
213 let workspace_relative = p.strip_prefix(workspace_root).unwrap();
214 let mut package_path = workspace_relative.to_path_buf();
215 for _ in target.components() {
216 package_path.pop();
217 }
218
219 let package = if package_path.components().count() > 0 {
220 path_to_label_part(&package_path)?
221 } else {
222 String::new()
223 };
224 let target = path_to_label_part(target)?;
225
226 Ok(Label::Absolute {
227 repository: Repository::Local,
228 package,
229 target,
230 })
231 }
232 (Some(_workspace_root), None) => {
233 bail!(
234 "Could not identify package for path {}. Maybe you need to add a BUILD.bazel file.",
235 p.display()
236 );
237 }
238 _ => {
239 bail!("Could not identify workspace for path {}", p.display());
240 }
241 }
242 }
243 }
244
245 /// Converts a path to a forward-slash-delimited label-appropriate path string.
path_to_label_part(path: &Path) -> Result<String, anyhow::Error>246 fn path_to_label_part(path: &Path) -> Result<String, anyhow::Error> {
247 let components: Result<Vec<_>, _> = path
248 .components()
249 .map(|c| {
250 c.as_os_str().to_str().ok_or_else(|| {
251 anyhow!(
252 "Found non-UTF8 component turning path into label: {}",
253 path.display()
254 )
255 })
256 })
257 .collect();
258 Ok(components?.join("/"))
259 }
260
261 impl Serialize for Label {
serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer,262 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
263 where
264 S: Serializer,
265 {
266 serializer.serialize_str(&self.repr())
267 }
268 }
269
270 struct LabelVisitor;
271 impl<'de> Visitor<'de> for LabelVisitor {
272 type Value = Label;
273
expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result274 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
275 formatter.write_str("Expected string value of `{name} {version}`.")
276 }
277
visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: serde::de::Error,278 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
279 where
280 E: serde::de::Error,
281 {
282 Label::from_str(v).map_err(E::custom)
283 }
284 }
285
286 impl<'de> Deserialize<'de> for Label {
deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>,287 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
288 where
289 D: serde::Deserializer<'de>,
290 {
291 deserializer.deserialize_str(LabelVisitor)
292 }
293 }
294
295 impl Label {
repr(&self) -> String296 pub(crate) fn repr(&self) -> String {
297 self.to_string()
298 }
299 }
300
301 #[cfg(test)]
302 mod test {
303 use super::*;
304 use spectral::prelude::*;
305 use std::fs::{create_dir_all, File};
306 use tempfile::tempdir;
307
308 #[test]
relative()309 fn relative() {
310 let label = Label::from_str(":target").unwrap();
311 assert_eq!(label.to_string(), ":target");
312 assert!(!label.is_absolute());
313 assert_eq!(label.repository(), None);
314 assert_eq!(label.package(), None);
315 assert_eq!(label.target(), "target");
316 }
317
318 #[test]
relative_implicit()319 fn relative_implicit() {
320 let label = Label::from_str("target").unwrap();
321 assert_eq!(label.to_string(), ":target");
322 assert!(!label.is_absolute());
323 assert_eq!(label.repository(), None);
324 assert_eq!(label.package(), None);
325 assert_eq!(label.target(), "target");
326 }
327
328 #[test]
absolute_full()329 fn absolute_full() {
330 let label = Label::from_str("@repo//package:target").unwrap();
331 assert_eq!(label.to_string(), "@repo//package:target");
332 assert!(label.is_absolute());
333 assert_eq!(
334 label.repository(),
335 Some(&Repository::Explicit(String::from("repo")))
336 );
337 assert_eq!(label.package(), Some("package"));
338 assert_eq!(label.target(), "target");
339 }
340
341 #[test]
absolute_repository()342 fn absolute_repository() {
343 let label = Label::from_str("@repo").unwrap();
344 assert_eq!(label.to_string(), "@repo//:repo");
345 assert!(label.is_absolute());
346 assert_eq!(
347 label.repository(),
348 Some(&Repository::Explicit(String::from("repo")))
349 );
350 assert_eq!(label.package(), Some(""));
351 assert_eq!(label.target(), "repo");
352 }
353
354 #[test]
absolute_package()355 fn absolute_package() {
356 let label = Label::from_str("//package").unwrap();
357 assert_eq!(label.to_string(), "//package:package");
358 assert!(label.is_absolute());
359 assert_eq!(label.repository(), Some(&Repository::Local));
360 assert_eq!(label.package(), Some("package"));
361 assert_eq!(label.target(), "package");
362
363 let label = Label::from_str("//package/subpackage").unwrap();
364 assert_eq!(label.to_string(), "//package/subpackage:subpackage");
365 assert!(label.is_absolute());
366 assert_eq!(label.repository(), Some(&Repository::Local));
367 assert_eq!(label.package(), Some("package/subpackage"));
368 assert_eq!(label.target(), "subpackage");
369 }
370
371 #[test]
absolute_target()372 fn absolute_target() {
373 let label = Label::from_str("//:target").unwrap();
374 assert_eq!(label.to_string(), "//:target");
375 assert!(label.is_absolute());
376 assert_eq!(label.repository(), Some(&Repository::Local));
377 assert_eq!(label.package(), Some(""));
378 assert_eq!(label.target(), "target");
379 }
380
381 #[test]
absolute_repository_package()382 fn absolute_repository_package() {
383 let label = Label::from_str("@repo//package").unwrap();
384 assert_eq!(label.to_string(), "@repo//package:package");
385 assert!(label.is_absolute());
386 assert_eq!(
387 label.repository(),
388 Some(&Repository::Explicit(String::from("repo")))
389 );
390 assert_eq!(label.package(), Some("package"));
391 assert_eq!(label.target(), "package");
392 }
393
394 #[test]
absolute_repository_target()395 fn absolute_repository_target() {
396 let label = Label::from_str("@repo//:target").unwrap();
397 assert_eq!(label.to_string(), "@repo//:target");
398 assert!(label.is_absolute());
399 assert_eq!(
400 label.repository(),
401 Some(&Repository::Explicit(String::from("repo")))
402 );
403 assert_eq!(label.package(), Some(""));
404 assert_eq!(label.target(), "target");
405 }
406
407 #[test]
absolute_package_target()408 fn absolute_package_target() {
409 let label = Label::from_str("//package:target").unwrap();
410 assert_eq!(label.to_string(), "//package:target");
411 assert!(label.is_absolute());
412 assert_eq!(label.repository(), Some(&Repository::Local));
413 assert_eq!(label.package(), Some("package"));
414 assert_eq!(label.target(), "target");
415 }
416
417 #[test]
invalid_empty()418 fn invalid_empty() {
419 Label::from_str("").unwrap_err();
420 Label::from_str("@").unwrap_err();
421 Label::from_str("//").unwrap_err();
422 Label::from_str(":").unwrap_err();
423 }
424
425 #[test]
invalid_relative_repository_package_target()426 fn invalid_relative_repository_package_target() {
427 Label::from_str("@repo/package:target").unwrap_err();
428 }
429
430 #[test]
invalid_relative_repository_package()431 fn invalid_relative_repository_package() {
432 Label::from_str("@repo/package").unwrap_err();
433 }
434
435 #[test]
invalid_relative_repository_target()436 fn invalid_relative_repository_target() {
437 Label::from_str("@repo:target").unwrap_err();
438 }
439
440 #[test]
invalid_relative_package_target()441 fn invalid_relative_package_target() {
442 Label::from_str("package:target").unwrap_err();
443 }
444
445 #[test]
full_label_bzlmod()446 fn full_label_bzlmod() {
447 let label = Label::from_str("@@repo//package/sub_package:target").unwrap();
448 assert_eq!(label.to_string(), "@@repo//package/sub_package:target");
449 assert!(label.is_absolute());
450 assert_eq!(
451 label.repository(),
452 Some(&Repository::Canonical(String::from("repo")))
453 );
454 assert_eq!(label.package(), Some("package/sub_package"));
455 assert_eq!(label.target(), "target");
456 }
457
458 #[test]
full_label_bzlmod_with_tilde()459 fn full_label_bzlmod_with_tilde() {
460 let label = Label::from_str("@@repo~name//package/sub_package:target").unwrap();
461 assert_eq!(label.to_string(), "@@repo~name//package/sub_package:target");
462 assert!(label.is_absolute());
463 assert_eq!(
464 label.repository(),
465 Some(&Repository::Canonical(String::from("repo~name")))
466 );
467 assert_eq!(label.package(), Some("package/sub_package"));
468 assert_eq!(label.target(), "target");
469 }
470
471 #[test]
full_label_with_slash_after_colon()472 fn full_label_with_slash_after_colon() {
473 let label = Label::from_str("@repo//package/sub_package:subdir/target").unwrap();
474 assert_eq!(
475 label.to_string(),
476 "@repo//package/sub_package:subdir/target"
477 );
478 assert!(label.is_absolute());
479 assert_eq!(
480 label.repository(),
481 Some(&Repository::Explicit(String::from("repo")))
482 );
483 assert_eq!(label.package(), Some("package/sub_package"));
484 assert_eq!(label.target(), "subdir/target");
485 }
486
487 #[test]
label_contains_plus()488 fn label_contains_plus() {
489 let label = Label::from_str("@repo//vendor/wasi-0.11.0+wasi-snapshot-preview1:BUILD.bazel")
490 .unwrap();
491 assert!(label.is_absolute());
492 assert_eq!(
493 label.repository(),
494 Some(&Repository::Explicit(String::from("repo")))
495 );
496 assert_eq!(
497 label.package(),
498 Some("vendor/wasi-0.11.0+wasi-snapshot-preview1")
499 );
500 assert_eq!(label.target(), "BUILD.bazel");
501 }
502
503 #[test]
invalid_double_colon()504 fn invalid_double_colon() {
505 Label::from_str("::target").unwrap_err();
506 }
507
508 #[test]
invalid_triple_at()509 fn invalid_triple_at() {
510 Label::from_str("@@@repo//pkg:target").unwrap_err();
511 }
512
513 #[test]
from_absolute_path_exists()514 fn from_absolute_path_exists() {
515 let dir = tempdir().unwrap();
516 let workspace = dir.path().join("WORKSPACE.bazel");
517 let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
518 let subdir = dir.path().join("parent").join("child").join("grandchild");
519 let actual_file = subdir.join("greatgrandchild");
520 create_dir_all(subdir).unwrap();
521 {
522 File::create(workspace).unwrap();
523 File::create(build_file).unwrap();
524 File::create(&actual_file).unwrap();
525 }
526 let label = Label::from_absolute_path(&actual_file).unwrap();
527 assert_eq!(
528 label.to_string(),
529 "//parent/child:grandchild/greatgrandchild"
530 );
531 assert!(label.is_absolute());
532 assert_eq!(label.repository(), Some(&Repository::Local));
533 assert_eq!(label.package(), Some("parent/child"));
534 assert_eq!(label.target(), "grandchild/greatgrandchild");
535 }
536
537 #[test]
from_absolute_path_no_workspace()538 fn from_absolute_path_no_workspace() {
539 let dir = tempdir().unwrap();
540 let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
541 let subdir = dir.path().join("parent").join("child").join("grandchild");
542 let actual_file = subdir.join("greatgrandchild");
543 create_dir_all(subdir).unwrap();
544 {
545 File::create(build_file).unwrap();
546 File::create(&actual_file).unwrap();
547 }
548 let err = Label::from_absolute_path(&actual_file)
549 .unwrap_err()
550 .to_string();
551 assert_that(&err).contains("Could not identify workspace");
552 assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
553 }
554
555 #[test]
from_absolute_path_no_build_file()556 fn from_absolute_path_no_build_file() {
557 let dir = tempdir().unwrap();
558 let workspace = dir.path().join("WORKSPACE.bazel");
559 let subdir = dir.path().join("parent").join("child").join("grandchild");
560 let actual_file = subdir.join("greatgrandchild");
561 create_dir_all(subdir).unwrap();
562 {
563 File::create(workspace).unwrap();
564 File::create(&actual_file).unwrap();
565 }
566 let err = Label::from_absolute_path(&actual_file)
567 .unwrap_err()
568 .to_string();
569 assert_that(&err).contains("Could not identify package");
570 assert_that(&err).contains("Maybe you need to add a BUILD.bazel file");
571 assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
572 }
573 }
574