stellar_build/
deps.rs

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