Skip to main content

xtask_toolkit/
cargo.rs

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}