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}