1use std::fs::read_dir;
2use std::path::{Path, PathBuf};
3use std::{env, fs};
4
5use xshell::{Shell, cmd};
6
7#[derive(Debug, thiserror::Error)]
8pub enum ProjectRootError {
9 #[error("Unspecified IO error during project root discovery: {0}")]
10 Io(std::io::Error),
11
12 #[error("Project root (Cargo.lock) cannot be found")]
13 MissingCargoLock,
14}
15
16impl From<std::io::Error> for ProjectRootError {
17 fn from(e: std::io::Error) -> Self {
18 ProjectRootError::Io(e)
19 }
20}
21
22pub fn get_project_root() -> Result<PathBuf, ProjectRootError> {
23 let path = env::current_dir()?;
24 let path_ancestors = path.as_path().ancestors();
25
26 for p in path_ancestors {
27 let has_cargo = read_dir(p)?.any(|p| p.unwrap().file_name() == *"Cargo.lock");
28 if has_cargo {
29 return Ok(PathBuf::from(p));
30 }
31 }
32 Err(ProjectRootError::MissingCargoLock)
33}
34
35#[derive(Debug, Clone)]
36pub struct CargoToml(PathBuf);
37
38impl CargoToml {
39
40 pub fn autodiscovery() -> Vec<Self> {
41 Self::autodiscovery_with(&[])
42 }
43
44 pub fn autodiscovery_with(additional_filenames: &[&str]) -> Vec<Self> {
45 get_project_root().map(|p| Self::find_all(&p, additional_filenames)).unwrap_or_default()
46 }
47
48 pub fn find_all<P: AsRef<Path>>(dir: P, additional_filenames: &[&str]) -> Vec<Self> {
49 let mut matches = Vec::new();
50 let target_name = "Cargo.toml";
51
52 if let Ok(entries) = fs::read_dir(&dir) {
53 for entry in entries.flatten() {
54 let path = entry.path();
55
56 if path.is_dir() {
57 matches.extend(Self::find_all(&path, additional_filenames));
58 } else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
59 if file_name == target_name || additional_filenames.contains(&file_name) {
60 matches.push(Self(path));
61 }
62 }
63 }
64 }
65
66 matches
67 }
68 pub fn find_first<P: AsRef<Path>>(dir: P, additional_filenames: &[&str]) -> Option<Self> {
69 let target_name = "Cargo.toml";
70
71 let mut entries = match fs::read_dir(&dir) {
72 Ok(e) => e.filter_map(Result::ok).collect::<Vec<_>>(),
73 Err(_) => return None,
74 };
75
76 entries.sort_by_key(|e| e.path());
77
78 for entry in &entries {
79 let path = entry.path();
80
81 if path.is_file() {
82 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
83 if name == target_name || additional_filenames.contains(&name) {
84 return Some(Self(path));
85 }
86 }
87 }
88 }
89
90 for entry in entries {
91 let path = entry.path();
92 if path.is_dir() {
93 if let Some(found) = Self::find_first(&path, additional_filenames) {
94 return Some(found);
95 }
96 }
97 }
98
99 None
100 }
101
102 fn get_toml_key<'a, T>(&self, keypath: &[&str]) -> Option<T>
103 where
104 T: serde::Deserialize<'a>,
105 {
106 let mut result = toml::from_str::<toml::Value>(&fs::read_to_string(&self.0).ok()?).ok()?;
107
108 for key in keypath {
109 if result.is_table() && result.get(key).is_some() {
110 let table = result.as_table_mut().unwrap();
111 result = table.remove(*key).unwrap();
112 } else {
113 break;
114 }
115 }
116
117 result.try_into().ok()
118 }
119
120 pub fn path<'a>(&self) -> &Path {
121 &self.0
122 }
123
124 pub fn version(&self) -> Option<String> {
125 self.get_toml_key(&["package", "version"])
126 }
127
128 pub fn name(&self) -> Option<String> {
129 self.get_toml_key(&["package", "name"])
130 }
131
132 pub fn license(&self) -> Option<String> {
133 self.get_toml_key(&["package", "license"])
134 }
135
136 pub fn authors(&self) -> Option<Vec<String>> {
137 self.get_toml_key(&["package", "authors"])
138 }
139
140 pub fn description(&self) -> Option<String> {
141 self.get_toml_key(&["package", "description"])
142 }
143
144 pub fn versioned_name(&self) -> Option<String> {
145 let name = self.name();
146 let version = self.version();
147 name.zip(version).map(|(name, version)| format!("{}-{}", name, version))
148 }
149}
150
151pub struct BinaryBuild {
152 projects: Vec<String>,
153 target: Option<String>,
154}
155
156impl BinaryBuild {
157 pub fn new() -> Self {
158 Self {
159 projects: Vec::new(),
160 target: None,
161 }
162 }
163
164 pub fn with_project(&mut self, project: &str) -> &mut Self {
165 self.projects.push(project.to_string());
166 self
167 }
168
169 pub fn with_projects<T, I>(&mut self, projects: I) -> &mut Self
170 where
171 I: IntoIterator<Item = T>,
172 T: ToString,
173 {
174 self.projects.extend(projects.into_iter().map(|x| x.to_string()));
175 self
176 }
177
178 pub fn with_target(&mut self, target: &str) -> &mut Self {
179 self.target = Some(target.to_string());
180 self
181 }
182
183 pub fn build(&self) -> Result<(), xshell::Error> {
184 let sh = Shell::new()?;
185
186 let projects: Vec<String> = self.projects.iter().map(|x| format!("-p={}", x)).collect();
187
188 let cmd = sh.cmd("cargo").args([
189 "build",
190 "--release",
191 ]);
192
193 let cmd = if let Some(target) = &self.target {
194 cmd.args(["--target", target])
195 } else {
196 cmd
197 };
198
199 cmd.args(projects).run()?;
200
201 Ok(())
202 }
203}
204
205pub fn force_fmt() -> Result<(), xshell::Error> {
206 let sh = Shell::new()?;
207 cmd!(sh, "cargo fmt").read()?;
208 cmd!(sh, "cargo clippy --fix --allow-dirty --allow-staged").read()?;
209 Ok(())
210}