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}