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 && r == &p
132 {
133 return None;
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 let network = std::env::var("STELLAR_NETWORK").unwrap_or_else(|_| "local".to_owned());
145 target_dir.join("stellar").join(network)
146}
147
148#[must_use]
149pub fn stellar_wasm_out_file(target_dir: &Path, name: &str) -> PathBuf {
150 stellar_wasm_out_dir(target_dir)
151 .join(name.replace('-', "_"))
152 .with_extension("wasm")
153}
154// /// Retrieves a list of source and output paths for dependencies of a specified kind.
155// ///
156// /// # Arguments
157// ///
158// /// * `manifest_path` - The path to the Cargo.toml manifest file.
159// /// * `kind` - The kind of dependency to filter.
160// ///
161// /// # Returns
162// ///
163// /// A `Result` containing a vector of tuples, where each tuple contains:
164// /// - The path to the dependency's `lib.rs` file
165// /// - The output directory for the dependency
166// ///
167// /// # Errors
168// ///
169// /// This function can return an error in the following cases:
170// /// - If there's an issue reading or parsing the manifest file
171// /// - If a dependency's manifest path doesn't have a parent directory
172// /// - If there are any issues accessing or processing the dependency information
173// pub fn all_contracts(manifest_path: &Path) -> Result<Vec<(Utf8PathBuf, PathBuf)>, Error> {
174// all(manifest_path)?
175// .into_iter()
176// .filter(|p| p.is_dep() || p.manifest_path == manifest_path)
177// .map(|p| {
178// let version = &p.version;
179// let name = &p.name;
180// let dir = PathBuf::from(format!("{name}{version}"));
181// let out_dir = out_dir(&dir, name);
182// let res = (
183// p.manifest_path
184// .parent()
185// .ok_or_else(|| Error::ParentNotFound(p.manifest_path.to_path_buf().into()))?
186// .join("src")
187// .join("lib.rs"),
188// out_dir,
189// );
190// Ok(res)
191// })
192// .collect::<Result<HashSet<_>, Error>>()
193// .map(IntoIterator::into_iter)
194// .map(Iterator::collect::<Vec<_>>)
195// }
196
197/// Retrieves a list of contract dependencies for a given manifest path.
198///
199/// This function filters all dependencies of the package specified by the manifest path,
200/// returning only those that are of the Contract kind and are not the package itself.
201///
202/// # Arguments
203///
204/// * `manifest_path` - A Path to the Cargo.toml manifest file.
205///
206/// # Returns
207///
208/// A Result containing a Vec of Package structs representing the contract dependencies,
209/// or an Error if the operation fails.
210///
211/// # Errors
212///
213/// This function will return an Error if:
214/// * There's an issue reading or parsing the manifest file.
215/// * There's a problem retrieving the dependencies.
216pub fn contract(manifest_path: &Path) -> Result<Vec<Package>, Error> {
217 Ok(all(manifest_path)?
218 .into_iter()
219 .filter(|p| p.is_dep() && p.manifest_path != manifest_path)
220 .collect())
221}
222
223/// Constructs a workspace from a list of packages, sorting them topologically based on their contract dependencies.
224///
225/// This function creates a dependency graph of the provided packages and their contract dependencies,
226/// then returns a topologically sorted list of these packages.
227///
228/// # Arguments
229///
230/// * `packages` - A slice of Package structs to process.
231///
232/// # Returns
233///
234/// A Result containing a Vec of Package structs representing the sorted workspace,
235/// or an Error if the operation fails.
236///
237/// # Errors
238///
239/// This function will return an Error if:
240/// * There's an issue retrieving contract dependencies for any of the packages.
241/// * The dependency graph contains cycles, making topological sorting impossible.
242pub fn get_workspace(packages: &[Package]) -> Result<Vec<Package>, Error> {
243 let mut graph: TopologicalSort<PackageId> = TopologicalSort::new();
244 for p in packages {
245 let contract_deps = contract(&p.manifest_path.clone().into_std_path_buf())?;
246 for dep in contract_deps {
247 graph.add_dependency(dep.id.clone(), p.id.clone());
248 }
249 graph.insert(p.id.clone());
250 }
251 let mut res = Vec::new();
252 while let Some(p) = graph.pop() {
253 if let Some(contract) = packages.iter().find(|p2| p2.id == p) {
254 res.push(contract.clone());
255 }
256 }
257 Ok(res)
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_get_soroban_deps() {
266 let pwd = std::env::current_dir().unwrap();
267 println!("{pwd:?}");
268 let manifest_path = pwd.join("../stellar-scaffold-test/fixtures/soroban-init-boilerplate/contracts/hello_world/Cargo.toml");
269 let mut c = cargo_metadata::MetadataCommand::new();
270 c.manifest_path(&manifest_path);
271 let metadata = c.exec().unwrap();
272 let normal = metadata.root_package().unwrap();
273 println!("{normal:#?}{}", normal.name);
274 let deps = all(&manifest_path).unwrap();
275 println!("{deps:#?}\n{}", deps.len());
276 }
277}