blob: a21c81e8a61a86d53d7c116fa4395e9e2ed78b0c [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> {
23 let re = Regex::new(r"^(@[\w\d\-_\.]*)?/{0,2}([\w\d\-_\./]+)?:?([\+\w\d\-_\./]+)$")?;
24 let cap = re
25 .captures(s)
26 .with_context(|| format!("Failed to parse label from string: {}", s))?;
27
28 let repository = cap
29 .get(1)
30 .map(|m| m.as_str().trim_start_matches('@').to_owned());
31 let package = cap.get(2).map(|m| m.as_str().to_owned());
32 let mut target = cap.get(3).map(|m| m.as_str().to_owned());
33
34 if target.is_none() {
35 if let Some(pkg) = &package {
36 target = Some(pkg.clone());
37 } else if let Some(repo) = &repository {
38 target = Some(repo.clone())
39 } else {
40 bail!("The label is missing a label")
41 }
42 }
43
44 // The target should be set at this point
45 let target = target.unwrap();
46
47 Ok(Self {
48 repository,
49 package,
50 target,
51 })
52 }
53}
54
55impl Display for Label {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Brian Silvermancc09f182022-03-09 15:40:20 -080057 // Add the repository
58 if let Some(repo) = &self.repository {
Brian Silverman5f6f2762022-08-13 19:30:05 -070059 write!(f, "@{}", repo)?;
Brian Silvermancc09f182022-03-09 15:40:20 -080060 }
61
Brian Silverman5f6f2762022-08-13 19:30:05 -070062 write!(f, "//")?;
63
Brian Silvermancc09f182022-03-09 15:40:20 -080064 // Add the package
65 if let Some(pkg) = &self.package {
Brian Silverman5f6f2762022-08-13 19:30:05 -070066 write!(f, "{}", pkg)?;
Brian Silvermancc09f182022-03-09 15:40:20 -080067 }
68
Brian Silverman5f6f2762022-08-13 19:30:05 -070069 write!(f, ":{}", self.target)?;
70
71 Ok(())
Brian Silvermancc09f182022-03-09 15:40:20 -080072 }
73}
74
75impl Label {
76 /// Generates a label appropriate for the passed Path by walking the filesystem to identify its
77 /// workspace and package.
78 pub fn from_absolute_path(p: &Path) -> Result<Self, anyhow::Error> {
79 let mut workspace_root = None;
80 let mut package_root = None;
81 for ancestor in p.ancestors().skip(1) {
82 if package_root.is_none()
83 && (ancestor.join("BUILD").exists() || ancestor.join("BUILD.bazel").exists())
84 {
85 package_root = Some(ancestor);
86 }
87 if workspace_root.is_none()
88 && (ancestor.join("WORKSPACE").exists()
89 || ancestor.join("WORKSPACE.bazel").exists())
90 {
91 workspace_root = Some(ancestor);
92 break;
93 }
94 }
95 match (workspace_root, package_root) {
96 (Some(workspace_root), Some(package_root)) => {
97 // These unwraps are safe by construction of the ancestors and prefix calls which set up these paths.
98 let target = p.strip_prefix(package_root).unwrap();
99 let workspace_relative = p.strip_prefix(workspace_root).unwrap();
100 let mut package_path = workspace_relative.to_path_buf();
101 for _ in target.components() {
102 package_path.pop();
103 }
104
105 let package = if package_path.components().count() > 0 {
106 Some(path_to_label_part(&package_path)?)
107 } else {
108 None
109 };
110 let target = path_to_label_part(target)?;
111
112 Ok(Label {
113 repository: None,
114 package,
115 target,
116 })
117 }
118 (Some(_workspace_root), None) => {
119 bail!(
120 "Could not identify package for path {}. Maybe you need to add a BUILD.bazel file.",
121 p.display()
122 );
123 }
124 _ => {
125 bail!("Could not identify workspace for path {}", p.display());
126 }
127 }
128 }
129}
130
131/// Converts a path to a forward-slash-delimited label-appropriate path string.
132fn path_to_label_part(path: &Path) -> Result<String, anyhow::Error> {
133 let components: Result<Vec<_>, _> = path
134 .components()
135 .map(|c| {
136 c.as_os_str().to_str().ok_or_else(|| {
137 anyhow!(
138 "Found non-UTF8 component turning path into label: {}",
139 path.display()
140 )
141 })
142 })
143 .collect();
144 Ok(components?.join("/"))
145}
146
147impl Serialize for Label {
148 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149 where
150 S: Serializer,
151 {
152 serializer.serialize_str(&self.repr())
153 }
154}
155
156struct LabelVisitor;
157impl<'de> Visitor<'de> for LabelVisitor {
158 type Value = Label;
159
160 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
161 formatter.write_str("Expected string value of `{name} {version}`.")
162 }
163
164 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
165 where
166 E: serde::de::Error,
167 {
168 Label::from_str(v).map_err(E::custom)
169 }
170}
171
172impl<'de> Deserialize<'de> for Label {
173 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174 where
175 D: serde::Deserializer<'de>,
176 {
177 deserializer.deserialize_str(LabelVisitor)
178 }
179}
180
181impl Label {
182 pub fn repr(&self) -> String {
183 self.to_string()
184 }
185}
186
187#[cfg(test)]
188mod test {
189 use super::*;
190 use spectral::prelude::*;
191 use std::fs::{create_dir_all, File};
192 use tempfile::tempdir;
193
194 #[test]
195 fn full_label() {
196 let label = Label::from_str("@repo//package/sub_package:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700197 assert_eq!(label.to_string(), "@repo//package/sub_package:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800198 assert_eq!(label.repository.unwrap(), "repo");
199 assert_eq!(label.package.unwrap(), "package/sub_package");
200 assert_eq!(label.target, "target");
201 }
202
203 #[test]
204 fn no_repository() {
205 let label = Label::from_str("//package:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700206 assert_eq!(label.to_string(), "//package:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800207 assert_eq!(label.repository, None);
208 assert_eq!(label.package.unwrap(), "package");
209 assert_eq!(label.target, "target");
210 }
211
212 #[test]
213 fn no_slashes() {
214 let label = Label::from_str("package:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700215 assert_eq!(label.to_string(), "//package:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800216 assert_eq!(label.repository, None);
217 assert_eq!(label.package.unwrap(), "package");
218 assert_eq!(label.target, "target");
219 }
220
221 #[test]
222 fn root_label() {
223 let label = Label::from_str("@repo//:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700224 assert_eq!(label.to_string(), "@repo//:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800225 assert_eq!(label.repository.unwrap(), "repo");
226 assert_eq!(label.package, None);
227 assert_eq!(label.target, "target");
228 }
229
230 #[test]
231 fn root_label_no_repository() {
232 let label = Label::from_str("//:target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700233 assert_eq!(label.to_string(), "//:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800234 assert_eq!(label.repository, None);
235 assert_eq!(label.package, None);
236 assert_eq!(label.target, "target");
237 }
238
239 #[test]
240 fn root_label_no_slashes() {
241 let label = Label::from_str(":target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700242 assert_eq!(label.to_string(), "//:target");
Brian Silvermancc09f182022-03-09 15:40:20 -0800243 assert_eq!(label.repository, None);
244 assert_eq!(label.package, None);
245 assert_eq!(label.target, "target");
246 }
247
248 #[test]
249 fn full_label_with_slash_after_colon() {
250 let label = Label::from_str("@repo//package/sub_package:subdir/target").unwrap();
Brian Silverman5f6f2762022-08-13 19:30:05 -0700251 assert_eq!(
252 label.to_string(),
253 "@repo//package/sub_package:subdir/target"
254 );
Brian Silvermancc09f182022-03-09 15:40:20 -0800255 assert_eq!(label.repository.unwrap(), "repo");
256 assert_eq!(label.package.unwrap(), "package/sub_package");
257 assert_eq!(label.target, "subdir/target");
258 }
259
260 #[test]
261 fn invalid_double_colon() {
262 assert!(Label::from_str("::target").is_err());
263 }
264
265 #[test]
266 fn invalid_double_at() {
267 assert!(Label::from_str("@@repo//pkg:target").is_err());
268 }
269
270 #[test]
271 #[ignore = "This currently fails. The Label parsing logic needs to be updated"]
272 fn invalid_no_double_slash() {
273 assert!(Label::from_str("@repo:target").is_err());
274 }
275
276 #[test]
277 fn from_absolute_path_exists() {
278 let dir = tempdir().unwrap();
279 let workspace = dir.path().join("WORKSPACE.bazel");
280 let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
281 let subdir = dir.path().join("parent").join("child").join("grandchild");
282 let actual_file = subdir.join("greatgrandchild");
283 create_dir_all(subdir).unwrap();
284 {
285 File::create(&workspace).unwrap();
286 File::create(&build_file).unwrap();
287 File::create(&actual_file).unwrap();
288 }
289 let label = Label::from_absolute_path(&actual_file).unwrap();
290 assert_eq!(label.repository, None);
291 assert_eq!(label.package.unwrap(), "parent/child");
292 assert_eq!(label.target, "grandchild/greatgrandchild")
293 }
294
295 #[test]
296 fn from_absolute_path_no_workspace() {
297 let dir = tempdir().unwrap();
298 let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
299 let subdir = dir.path().join("parent").join("child").join("grandchild");
300 let actual_file = subdir.join("greatgrandchild");
301 create_dir_all(subdir).unwrap();
302 {
303 File::create(&build_file).unwrap();
304 File::create(&actual_file).unwrap();
305 }
306 let err = Label::from_absolute_path(&actual_file)
307 .unwrap_err()
308 .to_string();
309 assert_that(&err).contains("Could not identify workspace");
310 assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
311 }
312
313 #[test]
314 fn from_absolute_path_no_build_file() {
315 let dir = tempdir().unwrap();
316 let workspace = dir.path().join("WORKSPACE.bazel");
317 let subdir = dir.path().join("parent").join("child").join("grandchild");
318 let actual_file = subdir.join("greatgrandchild");
319 create_dir_all(subdir).unwrap();
320 {
321 File::create(&workspace).unwrap();
322 File::create(&actual_file).unwrap();
323 }
324 let err = Label::from_absolute_path(&actual_file)
325 .unwrap_err()
326 .to_string();
327 assert_that(&err).contains("Could not identify package");
328 assert_that(&err).contains("Maybe you need to add a BUILD.bazel file");
329 assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
330 }
331}