wasmer_cli/commands/package/
tree.rs

1use 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/// Print a package's resolved dependency tree.
17#[derive(clap::Parser, Debug)]
18pub struct PackageTree {
19    #[clap(flatten)]
20    pub env: WasmerEnv,
21
22    /// Disable the cache for package metadata queries.
23    #[clap(long = "disable-cache")]
24    disable_cache: bool,
25
26    /// The package identifier to resolve, either a package name or sha256 hash.
27    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(), &registry_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}