wasmer_cli/commands/package/
push.rs

1use super::common::{macros::*, *};
2use crate::{
3    commands::{AsyncCliCommand, PackageBuild},
4    config::WasmerEnv,
5};
6use anyhow::Context;
7use bytes::Bytes;
8use colored::Colorize;
9use sha2::Digest;
10use std::io::IsTerminal as _;
11use std::path::{Path, PathBuf};
12use wasmer_backend_api::WasmerClient;
13use wasmer_config::package::{Manifest, PackageHash};
14
15/// Push a package to the registry from a `wasmer.toml` project or a raw `.webc` file.
16///
17/// The result of this operation is that the hash of the package can be used to reference the
18/// pushed package.
19#[derive(Debug, clap::Parser)]
20pub struct PackagePush {
21    #[clap(flatten)]
22    pub env: WasmerEnv,
23
24    /// Run the publish logic without sending anything to the registry server
25    #[clap(long, name = "dry-run")]
26    pub dry_run: bool,
27
28    /// Run the publish command without any output
29    #[clap(long)]
30    pub quiet: bool,
31
32    /// Override the namespace of the package to upload
33    #[clap(long = "namespace")]
34    pub package_namespace: Option<String>,
35
36    /// Override the name of the package to upload. If a name is specified,
37    /// no version will be attached to the package.
38    #[clap(long = "name")]
39    pub package_name: Option<String>,
40
41    /// Timeout (in seconds) for the publish query to the registry.
42    ///
43    /// Note that this is not the timeout for the entire publish process, but
44    /// for each individual query to the registry during the publish flow.
45    #[clap(long, default_value = "5m")]
46    pub timeout: humantime::Duration,
47
48    /// Do not prompt for user input.
49    #[clap(long, default_value_t = !std::io::stdin().is_terminal())]
50    pub non_interactive: bool,
51
52    /// Path to a package source:
53    /// - a directory containing `wasmer.toml`
54    /// - a custom `*.toml` manifest file
55    /// - a pre-built raw `*.webc` file
56    ///
57    /// Defaults to current working directory.
58    #[clap(name = "path", default_value = ".")]
59    pub package_path: PathBuf,
60}
61
62impl PackagePush {
63    async fn get_namespace(
64        &self,
65        client: &WasmerClient,
66        manifest: &Manifest,
67    ) -> anyhow::Result<String> {
68        if let Some(owner) = &self.package_namespace {
69            return Ok(owner.clone());
70        }
71
72        if let Some(pkg) = &manifest.package
73            && let Some(ns) = &pkg.name
74            && let Some(first) = ns.split('/').next()
75        {
76            return Ok(first.to_string());
77        }
78
79        if self.non_interactive {
80            // if not interactive we can't prompt the user to choose the owner of the app.
81            anyhow::bail!("No package namespace specified: use --namespace XXX");
82        }
83
84        let user = wasmer_backend_api::query::current_user_with_namespaces(client, None).await?;
85        let owner = crate::utils::prompts::prompt_for_namespace(
86            "Choose a namespace to push the package to",
87            None,
88            Some(&user),
89        )?;
90
91        Ok(owner.clone())
92    }
93
94    async fn get_name(&self, manifest: &Manifest) -> anyhow::Result<Option<String>> {
95        if let Some(name) = &self.package_name {
96            return Ok(Some(name.clone()));
97        }
98
99        if let Some(pkg) = &manifest.package
100            && let Some(ns) = &pkg.name
101            && let Some(name) = ns.split('/').nth(1)
102        {
103            return Ok(Some(name.to_string()));
104        }
105
106        Ok(None)
107    }
108
109    fn get_privacy(&self, manifest: &Manifest) -> bool {
110        match &manifest.package {
111            Some(pkg) => pkg.private,
112            None => true,
113        }
114    }
115
116    async fn should_push(&self, client: &WasmerClient, hash: &PackageHash) -> anyhow::Result<bool> {
117        let res = wasmer_backend_api::query::get_package_release(client, &hash.to_string()).await;
118        tracing::info!("{:?}", res);
119        res.map(|p| p.is_none())
120    }
121
122    async fn do_push(
123        &self,
124        client: &WasmerClient,
125        namespace: &str,
126        name: Option<String>,
127        package_bytes: Bytes,
128        package_hash: &PackageHash,
129        private: bool,
130    ) -> anyhow::Result<()> {
131        let pb = make_spinner!(self.quiet, "Uploading the package..");
132
133        let signed_url = upload(
134            client,
135            package_hash,
136            self.timeout,
137            package_bytes,
138            pb.clone(),
139            self.env.proxy()?,
140        )
141        .await?;
142        spinner_ok!(pb, "Package correctly uploaded");
143
144        let pb = make_spinner!(self.quiet, "Waiting for package to become available...");
145        match wasmer_backend_api::query::push_package_release(
146            client,
147            name.as_deref(),
148            namespace,
149            &signed_url,
150            Some(private),
151        )
152        .await?
153        {
154            Some(r) => {
155                if r.success {
156                    r.package_webc.unwrap().id
157                } else {
158                    anyhow::bail!(
159                        "An unidentified error occurred while publishing the package. (response had success: false)"
160                    )
161                }
162            }
163            None => anyhow::bail!("An unidentified error occurred while publishing the package."), // <- This is extremely bad..
164        };
165
166        let msg = format!("Succesfully pushed release to namespace {namespace} on the registry");
167        spinner_ok!(pb, msg);
168
169        Ok(())
170    }
171
172    pub async fn push(
173        &self,
174        client: &WasmerClient,
175        manifest: &Manifest,
176        manifest_path: &Path,
177    ) -> anyhow::Result<(String, PackageHash)> {
178        // Check if manifest_path is a .webc file
179        let is_webc = manifest_path.is_file()
180            && manifest_path.extension().and_then(|s| s.to_str()) == Some("webc");
181
182        let (hash, package_bytes) = if is_webc {
183            tracing::info!("Loading pre-built package from webc");
184            let pb = make_spinner!(self.quiet, "Loading the package...");
185
186            // Load the package from the webc file
187            let package_data = std::fs::read(manifest_path).with_context(|| {
188                format!("Failed to read webc file '{}'", manifest_path.display())
189            })?;
190
191            // Calculate hash
192            let hash_bytes: [u8; 32] = sha2::Sha256::digest(&package_data).into();
193            let hash = PackageHash::from_sha256_bytes(hash_bytes);
194
195            // Validate the webc file by parsing it (from_bytes consumes the data)
196            // TODO: avoid reading the whole file into memory.
197            let package_bytes = bytes::Bytes::from(package_data);
198            wasmer_package::utils::from_bytes(package_bytes.clone()).with_context(|| {
199                format!("Failed to parse webc file '{}'", manifest_path.display())
200            })?;
201
202            spinner_ok!(pb, "Correctly loaded pre-built package");
203
204            (hash, package_bytes)
205        } else {
206            tracing::info!("Building package");
207            let pb = make_spinner!(self.quiet, "Creating the package locally...");
208            let (package, hash) = PackageBuild::check(manifest_path.to_path_buf())
209                .execute()
210                .context("While trying to build the package locally")?;
211
212            spinner_ok!(pb, "Correctly built package locally");
213
214            // TODO: avoid keeping the whole package in memory for large packages.
215            let package_bytes = package.serialize()?;
216
217            (hash, package_bytes)
218        };
219
220        tracing::info!("Package has hash: {hash}");
221
222        let namespace = self.get_namespace(client, manifest).await?;
223        let name = self.get_name(manifest).await?;
224
225        let private = self.get_privacy(manifest);
226        tracing::info!(
227            "If published, package privacy is {private}, namespace is {namespace} and name is {name:?}"
228        );
229
230        let pb = make_spinner!(
231            self.quiet,
232            "Checking if package is already in the registry.."
233        );
234        if self.should_push(client, &hash).await.map_err(on_error)? {
235            if !self.dry_run {
236                tracing::info!("Package should be published");
237                pb.finish_and_clear();
238                // spinner_ok!(pb, "Package not in the registry yet!");
239
240                self.do_push(client, &namespace, name, package_bytes, &hash, private)
241                    .await
242                    .map_err(on_error)?;
243            } else {
244                tracing::info!("Package should be published, but dry-run is set");
245                spinner_ok!(pb, "Skipping push as dry-run is set");
246            }
247        } else {
248            tracing::info!("Package should not be published");
249            spinner_ok!(pb, "Package was already in the registry, no push needed");
250        }
251
252        tracing::info!("Proceeding to invalidate query cache..");
253
254        // Prevent `wasmer run` from using stale (cached) package versions after wasmer publish.
255        if let Err(e) = invalidate_graphql_query_cache(&self.env.cache_dir) {
256            tracing::warn!(
257                error = &*e,
258                "Unable to invalidate the cache used for package version queries",
259            );
260        }
261
262        Ok((namespace, hash))
263    }
264}
265
266#[async_trait::async_trait]
267impl AsyncCliCommand for PackagePush {
268    type Output = ();
269
270    async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
271        tracing::info!("Checking if user is logged in");
272        let client = login_user(&self.env, !self.non_interactive, "push a package").await?;
273
274        tracing::info!("Loading manifest");
275        let (manifest_path, manifest) = get_manifest(&self.package_path)?;
276        tracing::info!("Got manifest at path {}", manifest_path.display());
277
278        let (_, hash) = self.push(&client, &manifest, &manifest_path).await?;
279
280        let bin_name = bin_name!();
281        if let Some(package) = &manifest.package {
282            if package.name.is_some() {
283                let mut manifest_path_dir = manifest_path.clone();
284                manifest_path_dir.pop();
285
286                eprintln!(
287                    "{} You can now tag your package with `{}`",
288                    "ð–¥”".yellow().bold(),
289                    format!(
290                        "{bin_name} package tag {}{}",
291                        hash,
292                        if manifest_path_dir.canonicalize()? == std::env::current_dir()? {
293                            String::new()
294                        } else {
295                            format!(" {}", manifest_path_dir.display())
296                        }
297                    )
298                    .bold()
299                )
300            } else {
301                eprintln!(
302                    "{} Successfully pushed package ({hash})",
303                    "✔".green().bold()
304                );
305            }
306        } else {
307            eprintln!(
308                "{} Successfully pushed package ({hash})",
309                "✔".green().bold()
310            );
311        }
312
313        Ok(())
314    }
315}