wasmer_cli/commands/package/common/
mod.rs1use crate::{
2 commands::{AsyncCliCommand, Login},
3 config::WasmerEnv,
4 utils::load_package_manifest,
5};
6use bytes::Bytes;
7use colored::Colorize;
8use dialoguer::Confirm;
9use indicatif::{ProgressBar, ProgressStyle};
10use reqwest::Body;
11use std::path::{Path, PathBuf};
12use wasmer_backend_api::{WasmerClient, query::UploadMethod};
13use wasmer_config::package::{Manifest, NamedPackageIdent, PackageHash};
14
15pub mod macros;
16pub mod wait;
17
18pub(super) fn on_error(e: anyhow::Error) -> anyhow::Error {
19 #[cfg(feature = "telemetry")]
20 sentry::integrations::anyhow::capture_anyhow(&e);
21
22 e
23}
24
25pub(super) fn invalidate_graphql_query_cache(cache_dir: &Path) -> Result<(), anyhow::Error> {
31 let cache_dir = cache_dir.join("queries");
32 std::fs::remove_dir_all(cache_dir)?;
33
34 Ok(())
35}
36
37pub(super) async fn upload(
39 client: &WasmerClient,
40 hash: &PackageHash,
41 timeout: humantime::Duration,
42 bytes: Bytes,
43 pb: ProgressBar,
44 proxy: Option<reqwest::Proxy>,
45) -> anyhow::Result<String> {
46 let hash_str = hash.to_string();
47 let hash_str = hash_str.trim_start_matches("sha256:");
48
49 let session_uri = {
50 let default_timeout_secs = Some(60 * 30);
51 let q = wasmer_backend_api::query::get_signed_url_for_package_upload(
52 client,
53 default_timeout_secs,
54 Some(hash_str),
55 None,
56 None,
57 Some(UploadMethod::R2),
58 );
59
60 match q.await? {
61 Some(u) => u.url,
62 None => anyhow::bail!(
63 "The backend did not provide a valid signed URL to upload the package"
64 ),
65 }
66 };
67
68 tracing::info!("signed url is: {session_uri}");
69
70 let client = {
71 let builder = reqwest::Client::builder()
72 .default_headers(reqwest::header::HeaderMap::default())
73 .timeout(timeout.into());
74
75 let builder = if let Some(proxy) = proxy {
76 builder.proxy(proxy)
77 } else {
78 builder
79 };
80
81 builder.build().unwrap()
82 };
83
84 let total_bytes = bytes.len();
85 pb.set_length(total_bytes.try_into().unwrap());
86 pb.set_style(ProgressStyle::with_template("{spinner:.yellow} [{elapsed_precise}] [{bar:.white}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
87 .unwrap()
88 .progress_chars("█▉▊▋▌▍▎▏ ")
89 .tick_strings(&["✶", "✸", "✹", "✺", "✹", "✷", "✶"]));
90 tracing::info!("webc is {total_bytes} bytes long");
91
92 let chunk_size = 8 * 1024;
93
94 let stream = futures::stream::unfold(0, move |offset| {
95 let pb = pb.clone();
96 let bytes = bytes.clone();
97 async move {
98 if offset >= total_bytes {
99 return None;
100 }
101
102 let start = offset;
103
104 let end = if (start + chunk_size) >= total_bytes {
105 total_bytes
106 } else {
107 start + chunk_size
108 };
109
110 let n = end - start;
111 let next_chunk = bytes.slice(start..end);
112 pb.inc(n as u64);
113
114 Some((Ok::<_, std::io::Error>(next_chunk), offset + n))
115 }
116 });
117
118 let res = client
119 .put(&session_uri)
120 .header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
121 .header(reqwest::header::CONTENT_LENGTH, format!("{total_bytes}"))
122 .body(Body::wrap_stream(stream));
123
124 res.send()
125 .await
126 .map(|response| response.error_for_status())
127 .map_err(|e| anyhow::anyhow!("error uploading package to {session_uri}: {e}"))??;
128
129 Ok(session_uri)
130}
131
132pub(super) fn get_manifest(path: &Path) -> anyhow::Result<(PathBuf, Manifest)> {
137 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("webc") {
139 return Ok((path.to_path_buf(), get_manifest_from_webc_file(path)?));
140 }
141
142 load_package_manifest(path).and_then(|j| {
143 j.ok_or_else(|| anyhow::anyhow!("No valid manifest found in path '{}'", path.display()))
144 })
145}
146
147fn get_manifest_from_webc_file(path: &Path) -> anyhow::Result<Manifest> {
149 use wasmer_package::utils::from_disk;
150
151 let container = from_disk(path)
152 .map_err(|e| anyhow::anyhow!("Failed to load webc file '{}': {}", path.display(), e))?;
153
154 manifest_from_webc_metadata(container.manifest())
155}
156
157pub(super) fn manifest_from_webc_metadata(
163 webc_manifest: &webc::metadata::Manifest,
164) -> anyhow::Result<Manifest> {
165 let mut manifest = Manifest::new_empty();
167
168 let wapm_annotation = webc_manifest
170 .wapm()
171 .map_err(|e| anyhow::anyhow!("Failed to read package annotation from webc: {e}"))?;
172
173 if let Some(wapm) = wapm_annotation {
174 let mut package = wasmer_config::package::Package::new_empty();
175 package.name = wapm.name;
176 package.version = if let Some(v) = wapm.version {
177 Some(v.parse()?)
178 } else {
179 None
180 };
181 package.description = wapm.description;
182 package.license = wapm.license;
183 package.homepage = wapm.homepage;
184 package.repository = wapm.repository;
185 package.private = wapm.private;
186 package.entrypoint = webc_manifest.entrypoint.clone();
187
188 manifest.package = Some(package);
192 } else {
193 manifest.package = Some(wasmer_config::package::Package::new_empty());
195 }
196
197 Ok(manifest)
203}
204
205pub(super) async fn login_user(
206 env: &WasmerEnv,
207 interactive: bool,
208 msg: &str,
209) -> anyhow::Result<WasmerClient> {
210 if let Ok(client) = env.client() {
211 return Ok(client);
212 }
213
214 let theme = dialoguer::theme::ColorfulTheme::default();
215
216 if env.token().is_none() {
217 if interactive {
218 eprintln!(
219 "{}: You need to be logged in to {msg}.",
220 "WARN".yellow().bold()
221 );
222
223 if Confirm::with_theme(&theme)
224 .with_prompt("Do you want to login now?")
225 .interact()?
226 {
227 Login {
228 no_browser: false,
229 wasmer_dir: env.dir().to_path_buf(),
230 cache_dir: env.cache_dir().to_path_buf(),
231 token: None,
232 registry: env.registry.clone(),
233 }
234 .run_async()
235 .await?;
236 } else {
237 anyhow::bail!("Stopping the flow as the user is not logged in.")
238 }
239 } else {
240 let bin_name = self::macros::bin_name!();
241 eprintln!(
242 "You are not logged in. Use the `--token` flag or log in (use `{bin_name} login`) to {msg}."
243 );
244 anyhow::bail!("Stopping execution as the user is not logged in.")
245 }
246 }
247
248 env.client()
249}
250
251pub(super) fn registry_web_host(client: &WasmerClient) -> String {
254 let host = client.graphql_endpoint().domain().unwrap_or("wasmer.io");
255
256 match host {
258 _ if host.contains("wasmer.wtf") => "wasmer.wtf".to_string(),
259 _ if host.contains("wasmer.io") => "wasmer.io".to_string(),
260 _ => host.to_string(),
261 }
262}
263
264pub(super) fn package_web_url(
267 client: &WasmerClient,
268 full_name: &str,
269 version: Option<&str>,
270) -> String {
271 let host = registry_web_host(client);
272 match version {
273 Some(version) => format!("https://{host}/{full_name}@{version}"),
274 None => format!("https://{host}/{full_name}"),
275 }
276}
277
278pub(super) fn package_web_url_for_ident(client: &WasmerClient, pkg: &NamedPackageIdent) -> String {
280 let version = pkg.version_or_default().to_string().replace('=', "");
282 package_web_url(client, &pkg.full_name(), Some(&version))
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use anyhow::Context;
289 use humantime::Duration as HumanDuration;
290 use indicatif::ProgressBar;
291 use sha2::{Digest, Sha256};
292 use url::Url;
293 use wasmer_package::package::Package;
294
295 #[tokio::test]
296 #[ignore = "Requires WASMER_REGISTRY_URL/WASMER_TOKEN"]
297 async fn test_upload_package_r2() -> anyhow::Result<()> {
298 let registry = std::env::var("WASMER_REGISTRY_URL")
299 .context("set WASMER_REGISTRY_URL to point at the registry GraphQL endpoint")?;
300 let token = std::env::var("WASMER_TOKEN")
301 .context("set WASMER_TOKEN for the registry under test")?;
302 let client = WasmerClient::new(Url::parse(®istry)?, "wasmer-cli-upload-test")?
303 .with_auth_token(token);
304 let pkg_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
305 .join("../../wasmer-test-files/legacy/coreutils-1.0.11.tar.gz");
306 let package = Package::from_tarball_file(&pkg_path)?;
307 let bytes = package.serialize()?;
308 let hash_bytes: [u8; 32] = Sha256::digest(&bytes).into();
309 let hash = PackageHash::from_sha256_bytes(hash_bytes);
310 let pb = ProgressBar::hidden();
311
312 let upload_url = upload(
314 &client,
315 &hash,
316 HumanDuration::from(std::time::Duration::from_secs(300)),
317 package.serialize().unwrap(),
318 pb,
319 None,
320 )
321 .await?;
322 assert!(
323 upload_url.starts_with("http"),
324 "upload returned non-url: {upload_url}"
325 );
326 Ok(())
327 }
328
329 #[test]
330 fn test_get_manifest_from_webc() -> anyhow::Result<()> {
331 use tempfile::TempDir;
332 use wasmer_package::package::Package;
333
334 let temp_dir = TempDir::new()?;
336 let pkg_dir = temp_dir.path();
337
338 std::fs::write(
340 pkg_dir.join("wasmer.toml"),
341 r#"
342[package]
343name = "test/mypackage"
344version = "0.1.0"
345description = "Test package for webc manifest extraction"
346
347[fs]
348data = "data"
349"#,
350 )?;
351
352 std::fs::create_dir(pkg_dir.join("data"))?;
354 std::fs::write(pkg_dir.join("data/test.txt"), "Hello World")?;
355
356 let pkg = Package::from_manifest(pkg_dir.join("wasmer.toml"))?;
358 let webc_bytes = pkg.serialize()?;
359
360 let webc_path = pkg_dir.join("test.webc");
362 std::fs::write(&webc_path, &webc_bytes)?;
363
364 let (path, manifest) = get_manifest(&webc_path)?;
366
367 assert_eq!(path, webc_path);
368 assert!(
369 manifest.package.is_some(),
370 "manifest.package should be present"
371 );
372
373 let package = manifest.package.unwrap();
374
375 assert_eq!(
377 package.name, None,
378 "Package name should be None in webc (stripped by Package::from_manifest)"
379 );
380 assert_eq!(
381 package.version, None,
382 "Package version should be None in webc (stripped by Package::from_manifest)"
383 );
384 assert_eq!(
385 package.description, None,
386 "Package description should be None in webc (stripped by Package::from_manifest)"
387 );
388
389 Ok(())
390 }
391}