wasmer_cli/commands/package/
tree.rs1use std::sync::Arc;
2
3use anyhow::{Context, Result};
4use semver::{Op, Version, VersionReq};
5use wasmer_config::package::{PackageId, PackageIdent, PackageSource, Tag};
6use wasmer_wasix::runtime::resolver::{
7 BackendSource, Dependency, DependencyGraph, FileSystemSource, MultiSource, Source, WebSource,
8};
9
10use crate::{
11 commands::AsyncCliCommand,
12 config::WasmerEnv,
13 utils::{WAPM_SOURCE_CACHE_TIMEOUT, registry_query_cache_dir},
14};
15
16#[derive(clap::Parser, Debug)]
18pub struct PackageTree {
19 #[clap(flatten)]
20 pub env: WasmerEnv,
21
22 #[clap(long = "disable-cache")]
24 disable_cache: bool,
25
26 package: PackageIdent,
28}
29
30#[async_trait::async_trait]
31impl AsyncCliCommand for PackageTree {
32 type Output = ();
33
34 async fn run_async(self) -> Result<Self::Output> {
35 let source = self.prepare_source()?;
36 let package = PackageSource::Ident(self.package);
37
38 let root_summary = source.latest(&package).await.map_err(|error| {
39 wasmer_wasix::runtime::resolver::ResolveError::Registry {
40 package: package.clone(),
41 error,
42 }
43 })?;
44 let root_id = root_summary.package_id();
45
46 let graph = wasmer_wasix::runtime::resolver::resolve_dependency_graph(
47 &root_id,
48 &root_summary.pkg,
49 &source,
50 )
51 .await
52 .context("Dependency graph resolution failed")?;
53
54 println!("{}", format_root(&package, graph.id()));
55 print_dependencies(&graph, graph.id(), "");
56
57 wasmer_wasix::runtime::resolver::validate_dependency_graph(&graph)
58 .context("Dependency graph cannot be unified")?;
59
60 Ok(())
61 }
62}
63
64impl PackageTree {
65 fn prepare_source(&self) -> Result<MultiSource> {
66 let client =
67 wasmer_wasix::http::default_http_client().context("No HTTP client available")?;
68 let client = Arc::new(client);
69
70 let mut source = MultiSource::default();
71
72 let registry_endpoint = self.env.registry_endpoint()?;
73 let mut registry = BackendSource::new(registry_endpoint.clone(), client.clone());
74 if !self.disable_cache {
75 let cache_dir = registry_query_cache_dir(self.env.cache_dir(), ®istry_endpoint);
76 registry = registry.with_local_cache(cache_dir, WAPM_SOURCE_CACHE_TIMEOUT);
77 }
78 if let Some(token) = self.env.token() {
79 registry = registry.with_auth_token(token);
80 }
81 source.add_source(registry);
82
83 let downloads_cache_dir = self.env.cache_dir().join("downloads");
84 source.add_source(WebSource::new(downloads_cache_dir, client));
85 source.add_source(FileSystemSource::default());
86
87 Ok(source)
88 }
89}
90
91fn print_dependencies(graph: &DependencyGraph, package_id: &PackageId, prefix: &str) {
92 let dependencies = dependency_edges(graph, package_id);
93
94 for (index, (_alias, dependency, resolved_id)) in dependencies.iter().enumerate() {
95 let is_last = index + 1 == dependencies.len();
96 let connector = if is_last { "`-- " } else { "|-- " };
97
98 println!(
99 "{prefix}{connector}{}",
100 format_dependency(dependency, resolved_id)
101 );
102
103 let child_prefix = if is_last { " " } else { "| " };
104 print_dependencies(graph, resolved_id, &format!("{prefix}{child_prefix}"));
105 }
106}
107
108fn dependency_edges(
109 graph: &DependencyGraph,
110 package_id: &PackageId,
111) -> Vec<(String, Dependency, PackageId)> {
112 let dependency_ids = graph
113 .iter_dependencies()
114 .find(|(id, _)| *id == package_id)
115 .map(|(_, dependencies)| dependencies)
116 .unwrap_or_default();
117
118 let package = &graph[package_id].pkg;
119
120 dependency_ids
121 .into_iter()
122 .filter_map(|(alias, resolved_id)| {
123 let dependency = package
124 .dependencies
125 .iter()
126 .find(|dependency| dependency.alias() == alias)?;
127
128 Some((alias.to_string(), dependency.clone(), resolved_id.clone()))
129 })
130 .collect()
131}
132
133fn format_root(specified: &PackageSource, resolved_id: &PackageId) -> String {
134 if is_fixed_to_resolved(specified, resolved_id) {
135 resolved_id.to_string()
136 } else {
137 format!("{specified} -> {resolved_id}")
138 }
139}
140
141fn format_dependency(dependency: &Dependency, resolved_id: &PackageId) -> String {
142 if let (PackageSource::Ident(PackageIdent::Named(specified)), PackageId::Named(resolved)) =
143 (&dependency.pkg, resolved_id)
144 && specified.full_name() == resolved.full_name
145 {
146 let specified_version = specified
147 .tag
148 .as_ref()
149 .map_or("*".to_string(), |tag| tag.to_string());
150
151 if is_fixed_to_resolved(&dependency.pkg, resolved_id) {
152 return format!("{}@{specified_version}", specified.full_name());
153 }
154
155 return format!(
156 "{}@{specified_version}=>{}",
157 specified.full_name(),
158 resolved.version
159 );
160 }
161
162 if is_fixed_to_resolved(&dependency.pkg, resolved_id) {
163 dependency.pkg.to_string()
164 } else {
165 format!("{} -> {resolved_id}", dependency.pkg)
166 }
167}
168
169fn is_fixed_to_resolved(specified: &PackageSource, resolved_id: &PackageId) -> bool {
170 match (specified, resolved_id) {
171 (PackageSource::Ident(PackageIdent::Hash(specified)), PackageId::Hash(resolved)) => {
172 specified == resolved
173 }
174 (PackageSource::Ident(PackageIdent::Named(specified)), PackageId::Named(resolved)) => {
175 if specified.full_name() != resolved.full_name {
176 return false;
177 }
178
179 match &specified.tag {
180 Some(Tag::Named(tag)) => tag == &resolved.version.to_string(),
181 Some(Tag::VersionReq(req)) => version_req_is_exact(req, &resolved.version),
182 None => false,
183 }
184 }
185 _ => false,
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn format_dependency_never_includes_aliases() {
195 let dependency = Dependency {
196 alias: "logger".to_string(),
197 pkg: PackageSource::from(
198 wasmer_config::package::NamedPackageIdent::try_from_full_name_and_version(
199 "wasmer/log",
200 "^1",
201 )
202 .unwrap(),
203 ),
204 };
205 let resolved_id = PackageId::new_named("wasmer/log", "1.2.3".parse().unwrap());
206
207 assert_eq!(
208 format_dependency(&dependency, &resolved_id),
209 "wasmer/log@^1=>1.2.3"
210 );
211 }
212}
213
214fn version_req_is_exact(req: &VersionReq, version: &Version) -> bool {
215 let [comparator] = req.comparators.as_slice() else {
216 return false;
217 };
218
219 comparator.op == Op::Exact
220 && comparator.major == version.major
221 && comparator.minor == Some(version.minor)
222 && comparator.patch == Some(version.patch)
223 && comparator.pre == version.pre
224}