blob: c074a62d3aa9c7c88799da85cb8f87ef37f760cf [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001use std::fmt::{self, Display};
2use std::path::Path;
3use std::str::FromStr;
4
5use anyhow::{anyhow, bail, Context, Result};
6use regex::Regex;
7use serde::de::Visitor;
8use serde::{Deserialize, Serialize, Serializer};
9
Brian Silverman5f6f2762022-08-13 19:30:05 -070010// Note that this type assumes there's no such thing as a relative label;
11// `:foo` is assumed to be relative to the repo root, and parses out to equivalent to `//:foo`.
Brian Silvermancc09f182022-03-09 15:40:20 -080012#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone)]
13pub struct Label {
14 pub repository: Option<String>,
15 pub package: Option<String>,
16 pub target: String,
17}
18
19impl FromStr for Label {
20 type Err = anyhow::Error;
21
22 fn from_str(s: &str) -> Result<Self, Self::Err> {
Adam Snaider1c095c92023-07-08 02:09:58 -040023 let re = Regex::new(r"^(@@?[\w\d\-_\.]*)?/{0,2}([\w\d\-_\./+]+)?(:([\+\w\d\-_\./]+))?$")?;
Brian Silvermancc09f182022-03-09 15:40:20 -080024 let cap = re
25 .captures(s)
Adam Snaider1c095c92023-07-08 02:09:58 -040026 .with_context(|| format!("Failed to parse label from string: {s}"))?;
Brian Silvermancc09f182022-03-09 15:40:20 -080027
28 let repository = cap
29 .get(1)
30 .map(|m| m.as_str().trim_start_matches('@').to_owned());
Adam Snaider1c095c92023-07-08 02:09:58 -040031
Brian Silvermancc09f182022-03-09 15:40:20 -080032 let package = cap.get(2).map(|m| m.as_str().to_owned());
Adam Snaider1c095c92023-07-08 02:09:58 -040033 let mut target = cap.get(4).map(|m| m.as_str().to_owned());
Brian Silvermancc09f182022-03-09 15:40:20 -080034
35 if target.is_none() {
36 if let Some(pkg) = &package {
37 target = Some(pkg.clone());
38 } else if let Some(repo) = &repository {
39 target = Some(repo.clone())
40 } else {
41 bail!("The label is missing a label")
42 }
43 }
44
45 // The target should be set at this point
46 let target = target.unwrap();
47
48 Ok(Self {
49 repository,
50 package,
51 target,
52 })
53 }
54}
55
56impl Display for Label {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Brian Silvermancc09f182022-03-09 15:40:20 -080058 // Add the repository
59 if let Some(repo) = &self.repository {
Adam Snaider1c095c92023-07-08 02:09:58 -040060 write!(f, "@{repo}")?;
Brian Silvermancc09f182022-03-09 15:40:20 -080061 }
62
Brian Silverman5f6f2762022-08-13 19:30:05 -070063 write!(f, "//")?;
64
Brian Silvermancc09f182022-03-09 15:40:20 -080065 // Add the package
66 if let Some(pkg) = &self.package {
Adam Snaider1c095c92023-07-08 02:09:58 -040067 write!(f, "{pkg}")?;
Brian Silvermancc09f182022-03-09 15:40:20 -080068 }
69
Brian Silverman5f6f2762022-08-13 19:30:05 -070070 write!(f, ":{}", self.target)?;
71
72 Ok(())
Brian Silvermancc09f182022-03-09 15:40:20 -080073 }
74}
75
76impl Label {
77 /// Generates a label appropriate for the passed Path by walking the filesystem to identify its
78 /// workspace and package.
79 pub fn from_absolute_path(p: &Path) -> Result<Self, anyhow::Error> {
80 let mut workspace_root = None;
81 let mut package_root = None;
82 for ancestor in p.ancestors().skip(1) {
83 if package_root.is_none()
84 && (ancestor.join("BUILD").exists() || ancestor.join("BUILD.bazel").exists())
85 {
86 package_root = Some(ancestor);
87 }
88 if workspace_root.is_none()
89 && (ancestor.join("WORKSPACE").exists()
90 || ancestor.join("WORKSPACE.bazel").exists())
91 {
92 workspace_root = Some(ancestor);
93 break;
94 }
95 }
96 match (workspace_root, package_root) {
97 (Some(workspace_root), Some(package_root)) => {
98 // These unwraps are safe by construction of the ancestors and prefix calls which set up these paths.
99 let target = p.strip_prefix(package_root).unwrap();
100 let workspace_relative = p.strip_prefix(workspace_root).unwrap();
101 let mut package_path = workspace_relative.to_path_buf();
102 for _ in target.components() {
103 package_path.pop();
104 }
105
106 let package = if package_path.components().count() > 0 {
107 Some(path_to_label_part(&package_path)?)
108 } else {
109 None
110 };
111 let target = path_to_label_part(target)?;
112
113 Ok(Label {
114 repository: None,
115 package,
116 target,
117 })
118 }
119 (Some(_workspace_root), None) => {
120 bail!(
121 "Could not identify package for path {}. Maybe you need to add a BUILD.bazel file.",
122 p.display()
123 );
124 }
125 _ => {
126 bail!("Could not identify workspace for path {}", p.display());
127 }
128 }
129 }
130}
131
132/// Converts a path to a forward-slash-delimited label-appropriate path string.
133fn path_to_label_part(path: &Path) -> Result<String, anyhow::Error> {
134 let components: Result<Vec<_>, _> = path
135 .components()
136 .map(|c| {
137 c.as_os_str().to_str().ok_or_else(|| {
138 anyhow!(
139 "Found non-UTF8 component turning path into label: {}",
140 path.display()
141 )
142 })
143 })
144 .collect();
145 Ok(components?.join("/"))
146}
147
148impl Serialize for Label {
149 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150 where
151 S: Serializer,
152 {
153 serializer.serialize_str(&self.repr())
154 }
155}
156
157struct LabelVisitor;
158impl<'de> Visitor<'de> for LabelVisitor {
159 type Value = Label;
160
161 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
162 formatter.write_str("Expected string value of `{name} {version}`.")
163 }
164
165 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
166 where
167 E: serde::de::Error,
168 {
169 Label::from_str(v).map_err(E::custom)
170 }
171}
172
173impl<'de> Deserialize<'de> for Label {
174 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175 where
176 D: serde::Deserializer<'de>,
177 {
178 deserializer.deserialize_str(LabelVisitor)
179 }
180}
181
182impl Label {
183 pub fn repr(&self) -> String {
184 self.to_string()
185 }
186}
187
188#[cfg(test)]
189mod test {
190 use super::*;
191 use spectral::prelude::*;
192 use std::fs::{create_dir_all, File};
193 use tempfile::tempdir;
194
195 #[test]
Adam Snaider1c095c92023-07-08 02:09:58 -0400196 fn full_label_bzlmod() {
197 let label = Label::from_str("@@repo//package/sub_package:target").unwrap();
198 assert_eq!(label.to_string(), "@repo//package/sub_package:target");
199 assert_eq!(label.repository.unwrap(), "repo");
200 assert_eq!(label.package.unwrap(), "package/sub_package");
201 assert_eq!(label.target, "target");
202 }
203
204 #[test]
Brian Silvermancc09f182022-03-09 15:40:20 -0800205 fn full_label() {
206 let label = Label::from_str("@repo//package/sub_package:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700207 assert_eq!(label.to_string(), "@repo//package/sub_package:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800208 assert_eq!(label.repository.unwrap(), "repo");
209 assert_eq!(label.package.unwrap(), "package/sub_package");
210 assert_eq!(label.target, "target");
211 }
212
213 #[test]
214 fn no_repository() {
215 let label = Label::from_str("//package:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700216 assert_eq!(label.to_string(), "//package:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800217 assert_eq!(label.repository, None);
218 assert_eq!(label.package.unwrap(), "package");
219 assert_eq!(label.target, "target");
220 }
221
222 #[test]
223 fn no_slashes() {
224 let label = Label::from_str("package:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700225 assert_eq!(label.to_string(), "//package:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800226 assert_eq!(label.repository, None);
227 assert_eq!(label.package.unwrap(), "package");
228 assert_eq!(label.target, "target");
229 }
230
231 #[test]
232 fn root_label() {
233 let label = Label::from_str("@repo//:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700234 assert_eq!(label.to_string(), "@repo//:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800235 assert_eq!(label.repository.unwrap(), "repo");
236 assert_eq!(label.package, None);
237 assert_eq!(label.target, "target");
238 }
239
240 #[test]
241 fn root_label_no_repository() {
242 let label = Label::from_str("//:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700243 assert_eq!(label.to_string(), "//:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800244 assert_eq!(label.repository, None);
245 assert_eq!(label.package, None);
246 assert_eq!(label.target, "target");
247 }
248
249 #[test]
250 fn root_label_no_slashes() {
251 let label = Label::from_str(":target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700252 assert_eq!(label.to_string(), "//:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800253 assert_eq!(label.repository, None);
254 assert_eq!(label.package, None);
255 assert_eq!(label.target, "target");
256 }
257
258 #[test]
259 fn full_label_with_slash_after_colon() {
260 let label = Label::from_str("@repo//package/sub_package:subdir/target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700261 assert_eq!(
262 label.to_string(),
263 "@repo//package/sub_package:subdir/target"
264 );
Brian Silvermancc09f182022-03-09 15:40:20 -0800265 assert_eq!(label.repository.unwrap(), "repo");
266 assert_eq!(label.package.unwrap(), "package/sub_package");
267 assert_eq!(label.target, "subdir/target");
268 }
269
270 #[test]
Adam Snaider1c095c92023-07-08 02:09:58 -0400271 fn label_contains_plus() {
272 let label = Label::from_str("@repo//vendor/wasi-0.11.0+wasi-snapshot-preview1:BUILD.bazel")
273 .unwrap();
274 assert_eq!(label.repository.unwrap(), "repo");
275 assert_eq!(
276 label.package.unwrap(),
277 "vendor/wasi-0.11.0+wasi-snapshot-preview1"
278 );
279 assert_eq!(label.target, "BUILD.bazel");
280 }
281
282 #[test]
Brian Silvermancc09f182022-03-09 15:40:20 -0800283 fn invalid_double_colon() {
284 assert!(Label::from_str("::target").is_err());
285 }
286
287 #[test]
Adam Snaider1c095c92023-07-08 02:09:58 -0400288 fn invalid_triple_at() {
289 assert!(Label::from_str("@@@repo//pkg:target").is_err());
Brian Silvermancc09f182022-03-09 15:40:20 -0800290 }
291
292 #[test]
293 #[ignore = "This currently fails. The Label parsing logic needs to be updated"]
294 fn invalid_no_double_slash() {
295 assert!(Label::from_str("@repo:target").is_err());
296 }
297
298 #[test]
299 fn from_absolute_path_exists() {
300 let dir = tempdir().unwrap();
301 let workspace = dir.path().join("WORKSPACE.bazel");
302 let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
303 let subdir = dir.path().join("parent").join("child").join("grandchild");
304 let actual_file = subdir.join("greatgrandchild");
305 create_dir_all(subdir).unwrap();
306 {
Adam Snaider1c095c92023-07-08 02:09:58 -0400307 File::create(workspace).unwrap();
308 File::create(build_file).unwrap();
Brian Silvermancc09f182022-03-09 15:40:20 -0800309 File::create(&actual_file).unwrap();
310 }
311 let label = Label::from_absolute_path(&actual_file).unwrap();
312 assert_eq!(label.repository, None);
313 assert_eq!(label.package.unwrap(), "parent/child");
314 assert_eq!(label.target, "grandchild/greatgrandchild")
315 }
316
317 #[test]
318 fn from_absolute_path_no_workspace() {
319 let dir = tempdir().unwrap();
320 let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
321 let subdir = dir.path().join("parent").join("child").join("grandchild");
322 let actual_file = subdir.join("greatgrandchild");
323 create_dir_all(subdir).unwrap();
324 {
Adam Snaider1c095c92023-07-08 02:09:58 -0400325 File::create(build_file).unwrap();
Brian Silvermancc09f182022-03-09 15:40:20 -0800326 File::create(&actual_file).unwrap();
327 }
328 let err = Label::from_absolute_path(&actual_file)
329 .unwrap_err()
330 .to_string();
331 assert_that(&err).contains("Could not identify workspace");
332 assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
333 }
334
335 #[test]
336 fn from_absolute_path_no_build_file() {
337 let dir = tempdir().unwrap();
338 let workspace = dir.path().join("WORKSPACE.bazel");
339 let subdir = dir.path().join("parent").join("child").join("grandchild");
340 let actual_file = subdir.join("greatgrandchild");
341 create_dir_all(subdir).unwrap();
342 {
Adam Snaider1c095c92023-07-08 02:09:58 -0400343 File::create(workspace).unwrap();
Brian Silvermancc09f182022-03-09 15:40:20 -0800344 File::create(&actual_file).unwrap();
345 }
346 let err = Label::from_absolute_path(&actual_file)
347 .unwrap_err()
348 .to_string();
349 assert_that(&err).contains("Could not identify package");
350 assert_that(&err).contains("Maybe you need to add a BUILD.bazel file");
351 assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
352 }
353}