1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
use std::path::Path;
use anyhow::{Context, bail};
use dialoguer::console::style;
use wasmer_backend_api::WasmerClient;
use wasmer_config::package::{Manifest, PackageIdent, PackageSource};
use super::common::{
get_manifest, manifest_from_webc_metadata, package_web_url, registry_web_host,
};
use crate::commands::AsyncCliCommand;
use crate::config::WasmerEnv;
/// Show basic metadata of a package without unpacking or downloading it.
///
/// The package can be a directory containing a `wasmer.toml` file, a `.webc`
/// file, or a package name from the registry. If no argument is given, the
/// current directory is used.
#[derive(clap::Parser, Debug)]
pub struct PackageGet {
#[clap(flatten)]
pub env: WasmerEnv,
/// The package to show.
///
/// This can be a path to a package directory or a `.webc` file, or the name
/// of a package in the registry (e.g. `wasmer/hello@=0.1.0`).
#[clap(default_value = ".")]
pub package: String,
}
#[async_trait::async_trait]
impl AsyncCliCommand for PackageGet {
type Output = ();
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
let (manifest, web_url) = self.load_manifest().await?;
let print_field = |label: &str, value: &str| {
println!("{:<13} {value}", style(format!("{label}:")).bold().dim());
};
if let Some(package) = manifest.package.as_ref() {
print_field("Name", package.name.as_deref().unwrap_or("<unnamed>"));
print_field(
"Version",
&package
.version
.as_ref()
.map(|v| v.to_string())
.unwrap_or_else(|| "<none>".to_string()),
);
if let Some(description) = &package.description {
print_field("Description", description);
}
if let Some(license) = &package.license {
print_field("License", license);
}
if let Some(homepage) = &package.homepage {
print_field("Homepage", homepage);
}
if let Some(repository) = &package.repository {
print_field("Repository", repository);
}
if let Some(entrypoint) = &package.entrypoint {
print_field("Entrypoint", entrypoint);
}
if package.private {
print_field("Private", "true");
}
} else {
println!("{}", style("Package has no metadata.").dim());
}
if !manifest.commands.is_empty() {
let commands = manifest
.commands
.iter()
.map(|c| c.get_name())
.collect::<Vec<_>>()
.join(", ");
print_field("Commands", &commands);
}
if !manifest.dependencies.is_empty() {
print_field("Dependencies", &manifest.dependencies.len().to_string());
for (name, version) in &manifest.dependencies {
println!(" {name} = {version}");
}
}
if let Some(web_url) = web_url {
print_field("URL", &web_url);
}
Ok(())
}
}
impl PackageGet {
/// Resolve the `package` argument into a [`Manifest`], whether it points to a
/// local package or a package in the registry.
///
/// The second tuple element is a link to the package's page on the registry
/// web frontend, present only when the package was resolved from a named
/// registry lookup (local files and hashes have no such page).
async fn load_manifest(&self) -> anyhow::Result<(Manifest, Option<String>)> {
// A path on disk (directory with a `wasmer.toml`, or a `.webc` file)
// takes precedence over registry resolution.
let path = Path::new(&self.package);
if path.exists() {
let (_, manifest) = get_manifest(path)?;
return Ok((manifest, None));
}
let source: PackageSource = self.package.parse().with_context(|| {
format!(
"'{}' is not a file or directory on disk, or a valid package name",
self.package
)
})?;
match source {
PackageSource::Ident(PackageIdent::Named(id)) => {
let client = self.env.client_unauthennticated()?;
let version = id.version_or_default().to_string();
let version = if version == "*" {
String::from("latest")
} else {
version
};
let full_name = id.full_name();
let package = match wasmer_backend_api::query::get_package_version(
&client,
full_name.clone(),
version.clone(),
)
.await?
{
Some(package) => package,
None => {
// Echo the version the user actually typed rather than
// the semver requirement it parsed into (`4.4.4` would
// otherwise render as `^4.4.4`). `None` means the user
// gave no version (we resolved `latest`).
let requested = (version != "latest").then(|| {
self.package
.rsplit_once('@')
.map_or(version.as_str(), |(_, v)| v)
});
return Err(version_not_found_error(&client, &full_name, requested).await);
}
};
let json = package
.pirita_manifest
.as_ref()
.context("the registry did not return a manifest for this package")?;
let webc_manifest: webc::metadata::Manifest = serde_json::from_str(&json.0)
.context("could not parse the manifest returned by the registry")?;
let mut manifest = manifest_from_webc_metadata(&webc_manifest)?;
// Link to the package's page on the registry web frontend,
// using the concrete version the registry resolved for us.
let web_url = package_web_url(&client, &full_name, Some(&package.version));
// Prefer the authoritative metadata reported by the registry.
//
// `Package::from_manifest` strips name/version/description from
// the WAPM annotation when building the webc, so the manifest
// reconstructed from `pirita_manifest` is missing them. The
// registry exposes these fields directly, so re-inject them.
if let Some(pkg) = manifest.package.as_mut() {
pkg.name = Some(full_name);
pkg.version = Some(package.version.parse().with_context(|| {
format!(
"invalid version returned by the registry: '{}'",
package.version
)
})?);
// Take each registry field when present, otherwise keep
// whatever the webc carried (license/homepage/repository
// survive in the WAPM annotation; description does not).
if !package.description.is_empty() {
pkg.description = Some(package.description);
}
if package.license.is_some() {
pkg.license = package.license;
}
if package.homepage.is_some() {
pkg.homepage = package.homepage;
}
if package.repository.is_some() {
pkg.repository = package.repository;
}
}
Ok((manifest, Some(web_url)))
}
PackageSource::Ident(PackageIdent::Hash(hash)) => {
let client = self.env.client_unauthennticated()?;
let pkg = wasmer_backend_api::query::get_package_release(
&client,
&hash.to_string(),
)
.await?
.with_context(|| {
format!(
"Package with {hash} does not exist in the registry, or is not accessible"
)
})?;
let image = pkg
.webc_v3
.or(pkg.webc)
.context("the registry did not return a WebC image for this package")?;
let webc_manifest: webc::metadata::Manifest =
serde_json::from_str(&image.manifest.0)
.context("could not parse the manifest returned by the registry")?;
Ok((manifest_from_webc_metadata(&webc_manifest)?, None))
}
PackageSource::Path(p) => {
// Local paths will have already short-circuited at the very
// start of the command, so we should never get here,
// unless the local path is invalid or inaccessible.
bail!("no file or directory found at '{p}'")
}
PackageSource::Url(url) => {
bail!("showing a package directly from a URL is not supported: '{url}'")
}
}
}
}
/// Build a helpful error for when [`get_package_version`] returns nothing.
///
/// If the package exists but the requested version doesn't, the error lists the
/// versions that are available; otherwise it reports the package as not found.
///
/// [`get_package_version`]: wasmer_backend_api::query::get_package_version
async fn version_not_found_error(
client: &WasmerClient,
full_name: &str,
requested: Option<&str>,
) -> anyhow::Error {
let registry = registry_web_host(client);
match wasmer_backend_api::query::get_package_version_numbers(client, full_name.to_string())
.await
{
Ok(Some(mut versions)) if !versions.is_empty() => {
sort_versions(&mut versions);
let available = versions.join(", ");
match requested {
Some(version) => anyhow::anyhow!(
"package '{full_name}' has no version matching '{version}' in registry '{registry}'.\nAvailable versions: {available}"
),
None => anyhow::anyhow!(
"could not resolve the latest version of package '{full_name}' from registry '{registry}'.\nAvailable versions: {available}"
),
}
}
// The package exists but has no published versions, or doesn't exist.
Ok(_) => {
anyhow::anyhow!("package '{full_name}' was not found in registry '{registry}'")
}
// Couldn't list versions; fall back to a generic message.
Err(_) => anyhow::anyhow!(
"could not retrieve package information for package '{full_name}' from registry '{registry}'"
),
}
}
/// Sort version strings ascending, parsing them as semver where possible and
/// falling back to lexical order for anything that doesn't parse.
fn sort_versions(versions: &mut [String]) {
versions.sort_by(
|a, b| match (semver::Version::parse(a), semver::Version::parse(b)) {
(Ok(a), Ok(b)) => a.cmp(&b),
_ => a.cmp(b),
},
);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_cmd_package_get_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("wasmer.toml"),
r#"
[package]
name = "wasmer/test"
version = "1.2.3"
description = "A test package"
license = "MIT"
"#,
)
.unwrap();
let cmd = PackageGet {
env: WasmerEnv::new(
crate::config::DEFAULT_WASMER_CACHE_DIR.clone(),
crate::config::DEFAULT_WASMER_CACHE_DIR.clone(),
None,
None,
),
package: dir.path().to_str().unwrap().to_owned(),
};
cmd.run_async().await.unwrap();
}
}