loam_build/deps.rs
1use std::{
2 collections::{HashMap, HashSet},
3 fmt::Display,
4 path::{Path, PathBuf},
5 process::Command,
6};
7
8use cargo_metadata::{camino::Utf8PathBuf, Package, PackageId};
9use topological_sort::TopologicalSort;
10
11/// Retrieves the target directory for a Cargo project and appends "loam" to it.
12///
13/// This function uses `cargo_metadata` to get the target directory of a Cargo project
14/// specified by the given manifest path. It then appends "loam" to this path.
15///
16/// # Arguments
17///
18/// * `manifest_path` - A reference to a `Path` representing the location of the Cargo.toml file.
19///
20/// # Returns
21///
22/// Returns a `Result` containing:
23/// - `Ok(PathBuf)`: The path to the target directory with "loam" appended.
24/// - `Err(cargo_metadata::Error)`: If there's an error retrieving the metadata.
25///
26/// # Errors
27///
28/// This function will return an error if:
29/// - The manifest file cannot be found.
30/// - There's an issue executing the metadata command.
31/// - Any other error occurs during the metadata retrieval process.
32pub fn get_target_dir(manifest_path: &Path) -> Result<PathBuf, cargo_metadata::Error> {
33 Ok(cargo_metadata::MetadataCommand::new()
34 .manifest_path(manifest_path)
35 .exec()?
36 .target_directory
37 .to_path_buf()
38 .into_std_path_buf()
39 .join("loam"))
40}
41
42pub trait PackageExt {
43 fn is_dep(&self, key: &DepKind) -> bool;
44}
45
46impl PackageExt for Package {
47 /// Check if the package has the specified key in its metadata
48 fn is_dep(&self, key: &DepKind) -> bool {
49 #[allow(clippy::redundant_closure_for_method_calls)]
50 self.metadata
51 .as_object()
52 .and_then(|metadata| metadata.get("loam"))
53 .and_then(|subcontract| subcontract.as_object())
54 .and_then(|subcontract_object| subcontract_object.get(&key.to_string()))
55 .and_then(|export| export.as_bool())
56 .unwrap_or_default()
57 }
58}
59
60#[derive(thiserror::Error, Debug)]
61pub enum Error {
62 #[error("Failed to find root package with manifest_path {0:?}")]
63 RootNotFound(PathBuf),
64 #[error("Failed to cargo tree at manifest_path {0:?}")]
65 CargoTree(PathBuf),
66 #[error("Failed to get parent of {0}")]
67 ParentNotFound(PathBuf),
68 #[error(transparent)]
69 Metadata(#[from] cargo_metadata::Error),
70}
71
72/// Retrieves all dependencies for the given manifest path.
73///
74/// This function executes `cargo tree` to get the dependency tree and processes the output
75/// to return a vector of `Package` structs representing all dependencies, including the root package.
76///
77/// # Arguments
78///
79/// * `manifest_path` - A reference to the Path of the Cargo.toml file.
80///
81/// # Returns
82///
83/// A `Result` containing a `Vec<Package>` on success, or an `Error` on failure.
84///
85/// # Errors
86///
87/// This function will return an error in the following situations:
88/// - If the metadata command fails to execute
89/// - If the root package is not found in the metadata
90/// - If the parent directory of the manifest path cannot be determined
91/// - If the `cargo tree` command fails to execute
92///
93/// # Panics
94///
95/// This function may panic in the following situations:
96/// - If the output of `cargo tree` contains invalid UTF-8 characters
97/// - If the parsing of package names and versions from the `cargo tree` output fails
98pub fn all(manifest_path: &Path) -> Result<Vec<Package>, Error> {
99 let metadata = cargo_metadata::MetadataCommand::new()
100 .manifest_path(manifest_path)
101 .exec()?;
102
103 let p = metadata
104 .root_package()
105 .ok_or_else(|| Error::RootNotFound(manifest_path.to_path_buf()))?;
106
107 let packages = metadata
108 .packages
109 .iter()
110 .map(|p| (format!("{}v{}", p.name, p.version), p))
111 .collect::<HashMap<String, &Package>>();
112
113 let parent = manifest_path
114 .parent()
115 .ok_or_else(|| Error::ParentNotFound(manifest_path.to_path_buf()))?;
116 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
117 let output = Command::new(cargo)
118 .current_dir(parent)
119 .args(["tree", "--prefix", "none", "--edges", "normal"])
120 .output()
121 .map_err(|_| Error::CargoTree(parent.to_path_buf()))?;
122 let stdout = output.stdout;
123 let stdout_str = String::from_utf8(stdout).unwrap();
124
125 let mut res = stdout_str
126 .lines()
127 .filter_map(|line| {
128 let s: Vec<&str> = line.split(' ').collect();
129 let package_id = format!("{}{}", s[0], s[1]);
130 let res = packages.get(&package_id).copied();
131 if let Some(r) = &res {
132 if r == &p {
133 return None;
134 }
135 }
136 res.cloned()
137 })
138 .collect::<Vec<_>>();
139 res.push(p.clone());
140 Ok(res)
141}
142
143#[must_use]
144pub fn out_dir(target_dir: &Path, name: &str) -> PathBuf {
145 target_dir.join("loam").join(name.replace('-', "_"))
146}
147
148pub enum DepKind {
149 Subcontract,
150 Contract,
151}
152
153impl Display for DepKind {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 DepKind::Subcontract => write!(f, "subcontract"),
157 DepKind::Contract => write!(f, "contract"),
158 }
159 }
160}
161
162/// Retrieves a list of source and output paths for dependencies of a specified kind.
163///
164/// # Arguments
165///
166/// * `manifest_path` - The path to the Cargo.toml manifest file.
167/// * `kind` - The kind of dependency to filter.
168///
169/// # Returns
170///
171/// A `Result` containing a vector of tuples, where each tuple contains:
172/// - The path to the dependency's `lib.rs` file
173/// - The output directory for the dependency
174///
175/// # Errors
176///
177/// This function can return an error in the following cases:
178/// - If there's an issue reading or parsing the manifest file
179/// - If a dependency's manifest path doesn't have a parent directory
180/// - If there are any issues accessing or processing the dependency information
181pub fn loam(manifest_path: &Path, kind: &DepKind) -> Result<Vec<(Utf8PathBuf, PathBuf)>, Error> {
182 all(manifest_path)?
183 .into_iter()
184 .filter(|p| p.is_dep(kind) || p.manifest_path == manifest_path)
185 .map(|p| {
186 let version = &p.version;
187 let name = &p.name;
188 let dir = PathBuf::from(format!("{name}{version}"));
189 let out_dir = out_dir(&dir, name);
190 let res = (
191 p.manifest_path
192 .parent()
193 .ok_or_else(|| Error::ParentNotFound(p.manifest_path.to_path_buf().into()))?
194 .join("src")
195 .join("lib.rs"),
196 out_dir,
197 );
198 Ok(res)
199 })
200 .collect::<Result<HashSet<_>, Error>>()
201 .map(IntoIterator::into_iter)
202 .map(Iterator::collect::<Vec<_>>)
203}
204/// Retrieves subcontract dependencies from the given manifest path.
205///
206/// # Arguments
207///
208/// * `manifest_path` - The path to the Cargo.toml manifest file.
209///
210/// # Returns
211///
212/// A `Result` containing a vector of tuples, where each tuple contains:
213/// - A `Utf8PathBuf` representing the package name and version.
214/// - A `PathBuf` representing the path to the package.
215///
216/// # Errors
217///
218/// This function may return an error if:
219/// - The manifest file cannot be read or parsed.
220/// - There are issues accessing or processing dependency information.
221/// - Any other error occurs during the dependency resolution process.
222pub fn subcontract_paths(manifest_path: &Path) -> Result<Vec<(Utf8PathBuf, PathBuf)>, Error> {
223 loam(manifest_path, &DepKind::Subcontract)
224}
225
226/// Retrieves a list of contract dependencies for a given manifest path.
227///
228/// This function filters all dependencies of the package specified by the manifest path,
229/// returning only those that are of the Contract kind and are not the package itself.
230///
231/// # Arguments
232///
233/// * `manifest_path` - A Path to the Cargo.toml manifest file.
234///
235/// # Returns
236///
237/// A Result containing a Vec of Package structs representing the contract dependencies,
238/// or an Error if the operation fails.
239///
240/// # Errors
241///
242/// This function will return an Error if:
243/// * There's an issue reading or parsing the manifest file.
244/// * There's a problem retrieving the dependencies.
245pub fn contract(manifest_path: &Path) -> Result<Vec<Package>, Error> {
246 Ok(all(manifest_path)?
247 .into_iter()
248 .filter(|p| p.is_dep(&DepKind::Contract) && p.manifest_path != manifest_path)
249 .collect())
250}
251
252/// Retrieves a list of subcontract dependencies for a given manifest path.
253///
254/// This function filters all dependencies of the package specified by the manifest path,
255/// returning only those that are of the Contract kind and are not the package itself.
256///
257/// # Arguments
258///
259/// * `manifest_path` - A Path to the Cargo.toml manifest file.
260///
261/// # Returns
262///
263/// A Result containing a Vec of Package structs representing the contract dependencies,
264/// or an Error if the operation fails.
265///
266/// # Errors
267///
268/// This function will return an Error if:
269/// * There's an issue reading or parsing the manifest file.
270/// * There's a problem retrieving the dependencies.
271pub fn subcontract(manifest_path: &Path) -> Result<Vec<Package>, Error> {
272 Ok(all(manifest_path)?
273 .into_iter()
274 .filter(|p| p.is_dep(&DepKind::Subcontract))
275 .collect())
276}
277
278/// Constructs a workspace from a list of packages, sorting them topologically based on their contract dependencies.
279///
280/// This function creates a dependency graph of the provided packages and their contract dependencies,
281/// then returns a topologically sorted list of these packages.
282///
283/// # Arguments
284///
285/// * `packages` - A slice of Package structs to process.
286///
287/// # Returns
288///
289/// A Result containing a Vec of Package structs representing the sorted workspace,
290/// or an Error if the operation fails.
291///
292/// # Errors
293///
294/// This function will return an Error if:
295/// * There's an issue retrieving contract dependencies for any of the packages.
296/// * The dependency graph contains cycles, making topological sorting impossible.
297pub fn get_workspace(packages: &[Package]) -> Result<Vec<Package>, Error> {
298 let mut graph: TopologicalSort<PackageId> = TopologicalSort::new();
299 for p in packages {
300 let contract_deps = contract(&p.manifest_path.clone().into_std_path_buf())?;
301 for dep in contract_deps {
302 graph.add_dependency(dep.id.clone(), p.id.clone());
303 }
304 graph.insert(p.id.clone());
305 }
306 let mut res = Vec::new();
307 while let Some(p) = graph.pop() {
308 if let Some(contract) = packages.iter().find(|p2| p2.id == p) {
309 res.push(contract.clone());
310 }
311 }
312 Ok(res)
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_get_loam_deps() {
321 let pwd = std::env::current_dir().unwrap();
322 println!("{pwd:?}");
323 let manifest_path = pwd.join("../../test/normal/Cargo.toml");
324 let mut c = cargo_metadata::MetadataCommand::new();
325 c.manifest_path(&manifest_path);
326 let metadata = c.exec().unwrap();
327 let normal = metadata.root_package().unwrap();
328 println!("{normal:#?}{}", normal.name);
329 let deps = all(&manifest_path).unwrap();
330 println!("{deps:#?}\n{}", deps.len());
331 let deps = subcontract_paths(&manifest_path).unwrap();
332 println!("{deps:#?}\n{}", deps.len());
333 }
334}