wasmer_registry/
lib.rs

1//! High-level interactions with the Wasmer backend.
2//!
3//! The GraphQL schema can be updated by running `make` in the Wasmer repo's
4//! root directory.
5//!
6//! ```console
7//! $ make update-graphql-schema
8//! curl -sSfL https://registry.wasmer.io/graphql/schema.graphql > lib/registry/graphql/schema.graphql
9//! ```
10#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
11
12pub mod api;
13mod client;
14pub mod config;
15pub mod graphql;
16pub mod interface;
17pub mod login;
18pub mod package;
19pub mod publish;
20pub mod subscriptions;
21pub mod types;
22pub mod utils;
23pub mod wasmer_env;
24
25pub use crate::client::RegistryClient;
26
27use std::{
28    fmt,
29    path::{Path, PathBuf},
30    time::Duration,
31};
32
33use anyhow::Context;
34use graphql_client::GraphQLQuery;
35use tar::EntryType;
36
37use crate::utils::normalize_path;
38pub use crate::{
39    config::{format_graphql, WasmerConfig},
40    graphql::queries::get_bindings_query::ProgrammingLanguage,
41    package::Package,
42};
43
44pub static PACKAGE_TOML_FILE_NAME: &str = "wasmer.toml";
45pub static PACKAGE_TOML_FALLBACK_NAME: &str = "wapm.toml";
46pub static GLOBAL_CONFIG_FILE_NAME: &str = "wasmer.toml";
47
48#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
49#[non_exhaustive]
50pub struct PackageDownloadInfo {
51    pub registry: String,
52    pub package: String,
53    pub version: String,
54    pub is_latest_version: bool,
55    /// Is the package private?
56    pub is_private: bool,
57    pub commands: String,
58    pub manifest: String,
59    pub url: String,
60    pub pirita_url: Option<String>,
61}
62
63pub fn query_command_from_registry(
64    registry_url: &str,
65    command_name: &str,
66) -> Result<PackageDownloadInfo, String> {
67    use crate::graphql::{
68        execute_query,
69        queries::{get_package_by_command_query, GetPackageByCommandQuery},
70    };
71
72    let q = GetPackageByCommandQuery::build_query(get_package_by_command_query::Variables {
73        command_name: command_name.to_string(),
74    });
75
76    let response: get_package_by_command_query::ResponseData = execute_query(registry_url, "", &q)
77        .map_err(|e| format!("Error sending GetPackageByCommandQuery:  {e}"))?;
78
79    let command = response
80        .get_command
81        .ok_or_else(|| "GetPackageByCommandQuery: no get_command".to_string())?;
82
83    let package = command.package_version.package.display_name;
84    let version = command.package_version.version;
85    let url = command.package_version.distribution.download_url;
86    let pirita_url = command.package_version.distribution.pirita_download_url;
87
88    Ok(PackageDownloadInfo {
89        registry: registry_url.to_string(),
90        package,
91        version,
92        is_latest_version: command.package_version.is_last_version,
93        manifest: command.package_version.manifest,
94        commands: command_name.to_string(),
95        url,
96        pirita_url,
97        is_private: command.package_version.package.private,
98    })
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
102pub enum QueryPackageError {
103    ErrorSendingQuery(String),
104    NoPackageFound {
105        name: String,
106        version: Option<String>,
107    },
108}
109
110impl fmt::Display for QueryPackageError {
111    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112        match self {
113            QueryPackageError::ErrorSendingQuery(q) => write!(f, "error sending query: {q}"),
114            QueryPackageError::NoPackageFound { name, version } => {
115                write!(f, "no package found for {name:?}")?;
116                if let Some(version) = version {
117                    write!(f, " (version = {version:?})")?;
118                }
119
120                Ok(())
121            }
122        }
123    }
124}
125
126impl std::error::Error for QueryPackageError {}
127
128#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
129pub enum GetIfPackageHasNewVersionResult {
130    // if version = Some(...) and the ~/.wasmer/checkouts/.../{version} exists, the package is already installed
131    UseLocalAlreadyInstalled {
132        registry_host: String,
133        namespace: String,
134        name: String,
135        version: String,
136        path: PathBuf,
137    },
138    // if version = None, check for the latest version
139    LocalVersionMayBeOutdated {
140        registry_host: String,
141        namespace: String,
142        name: String,
143        /// Versions that are already installed + whether they are
144        /// older (true) or younger (false) than the timeout
145        installed_versions: Vec<(String, bool)>,
146    },
147    // registry / namespace / name / version doesn't exist yet
148    PackageNotInstalledYet {
149        registry_url: String,
150        namespace: String,
151        name: String,
152        version: Option<String>,
153    },
154}
155
156/// Returns the download info of the packages, on error returns all the available packages
157/// i.e. (("foo/python", "wasmer.io"), ("bar/python" "wasmer.io")))
158pub fn query_package_from_registry(
159    registry_url: &str,
160    name: &str,
161    version: Option<&str>,
162    auth_token: Option<&str>,
163) -> Result<PackageDownloadInfo, QueryPackageError> {
164    use crate::graphql::{
165        execute_query,
166        queries::{get_package_version_query, GetPackageVersionQuery},
167    };
168
169    let q = GetPackageVersionQuery::build_query(get_package_version_query::Variables {
170        name: name.to_string(),
171        version: version.map(|s| s.to_string()),
172    });
173
174    let response: get_package_version_query::ResponseData =
175        execute_query(registry_url, auth_token.unwrap_or_default(), &q).map_err(|e| {
176            QueryPackageError::ErrorSendingQuery(format!("Error sending GetPackagesQuery: {e}"))
177        })?;
178
179    let v = response
180        .package_version
181        .as_ref()
182        .ok_or_else(|| QueryPackageError::NoPackageFound {
183            name: name.to_string(),
184            version: None,
185        })?;
186
187    let manifest =
188        toml::from_str::<wasmer_config::package::Manifest>(&v.manifest).map_err(|e| {
189            QueryPackageError::ErrorSendingQuery(format!(
190                "Invalid manifest for crate {name:?}: {e}"
191            ))
192        })?;
193
194    Ok(PackageDownloadInfo {
195        registry: registry_url.to_string(),
196        package: v.package.name.clone(),
197
198        version: v.version.clone(),
199        is_latest_version: v.is_last_version,
200        is_private: v.package.private,
201        manifest: v.manifest.clone(),
202
203        commands: manifest
204            .commands
205            .iter()
206            .map(|s| s.get_name())
207            .collect::<Vec<_>>()
208            .join(", "),
209
210        url: v.distribution.download_url.clone(),
211        pirita_url: v.distribution.pirita_download_url.clone(),
212    })
213}
214
215pub fn get_checkouts_dir(wasmer_dir: &Path) -> PathBuf {
216    wasmer_dir.join("checkouts")
217}
218
219pub fn get_webc_dir(wasmer_dir: &Path) -> PathBuf {
220    wasmer_dir.join("webc")
221}
222
223/// Convenience function that will unpack .tar.gz files and .tar.bz
224/// files to a target directory (does NOT remove the original .tar.gz)
225pub fn try_unpack_targz<P: AsRef<Path>>(
226    target_targz_path: P,
227    target_path: P,
228    strip_toplevel: bool,
229) -> Result<PathBuf, anyhow::Error> {
230    let target_targz_path = target_targz_path.as_ref().to_string_lossy().to_string();
231    let target_targz_path = crate::utils::normalize_path(&target_targz_path);
232    let target_targz_path = Path::new(&target_targz_path);
233
234    let target_path = target_path.as_ref().to_string_lossy().to_string();
235    let target_path = crate::utils::normalize_path(&target_path);
236    let target_path = Path::new(&target_path);
237
238    let open_file = || {
239        std::fs::File::open(target_targz_path)
240            .map_err(|e| anyhow::anyhow!("failed to open {}: {e}", target_targz_path.display()))
241    };
242
243    let try_decode_gz = || {
244        let file = open_file()?;
245        let gz_decoded = flate2::read::GzDecoder::new(&file);
246        let ar = tar::Archive::new(gz_decoded);
247        if strip_toplevel {
248            unpack_sans_parent(ar, target_path).map_err(|e| {
249                anyhow::anyhow!(
250                    "failed to unpack (gz_ar_unpack_sans_parent) {}: {e}",
251                    target_targz_path.display()
252                )
253            })
254        } else {
255            unpack_with_parent(ar, target_path).map_err(|e| {
256                anyhow::anyhow!(
257                    "failed to unpack (gz_ar_unpack) {}: {e}",
258                    target_targz_path.display()
259                )
260            })
261        }
262    };
263
264    let try_decode_xz = || {
265        let file = open_file()?;
266        let mut decomp: Vec<u8> = Vec::new();
267        let mut bufread = std::io::BufReader::new(&file);
268        lzma_rs::xz_decompress(&mut bufread, &mut decomp).map_err(|e| {
269            anyhow::anyhow!(
270                "failed to unpack (try_decode_xz) {}: {e}",
271                target_targz_path.display()
272            )
273        })?;
274
275        let cursor = std::io::Cursor::new(decomp);
276        let mut ar = tar::Archive::new(cursor);
277        if strip_toplevel {
278            unpack_sans_parent(ar, target_path).map_err(|e| {
279                anyhow::anyhow!(
280                    "failed to unpack (sans parent) {}: {e}",
281                    target_targz_path.display()
282                )
283            })
284        } else {
285            ar.unpack(target_path).map_err(|e| {
286                anyhow::anyhow!(
287                    "failed to unpack (with parent) {}: {e}",
288                    target_targz_path.display()
289                )
290            })
291        }
292    };
293
294    try_decode_gz().or_else(|e1| {
295        try_decode_xz()
296            .map_err(|e2| anyhow::anyhow!("could not decode gz: {e1}, could not decode xz: {e2}"))
297    })?;
298
299    Ok(Path::new(&target_targz_path).to_path_buf())
300}
301
302/// Whether the top-level directory should be stripped
303pub fn download_and_unpack_targz(
304    url: &str,
305    target_path: &Path,
306    strip_toplevel: bool,
307) -> Result<PathBuf, anyhow::Error> {
308    let tempdir = tempfile::TempDir::new()?;
309
310    let target_targz_path = tempdir.path().join("package.tar.gz");
311
312    let mut resp = reqwest::blocking::get(url)
313        .map_err(|e| anyhow::anyhow!("failed to download {url}: {e}"))?;
314
315    {
316        let mut file = std::fs::File::create(&target_targz_path).map_err(|e| {
317            anyhow::anyhow!(
318                "failed to download {url} into {}: {e}",
319                target_targz_path.display()
320            )
321        })?;
322
323        resp.copy_to(&mut file)
324            .map_err(|e| anyhow::anyhow!("{e}"))?;
325    }
326
327    try_unpack_targz(target_targz_path.as_path(), target_path, strip_toplevel)
328        .with_context(|| anyhow::anyhow!("Could not download {url}"))?;
329
330    Ok(target_path.to_path_buf())
331}
332
333pub fn unpack_with_parent<R>(mut archive: tar::Archive<R>, dst: &Path) -> Result<(), anyhow::Error>
334where
335    R: std::io::Read,
336{
337    use std::path::Component::Normal;
338
339    let dst_normalized = normalize_path(dst.to_string_lossy().as_ref());
340
341    for entry in archive.entries()? {
342        let mut entry = entry?;
343        let path: PathBuf = entry
344            .path()?
345            .components()
346            .skip(1) // strip top-level directory
347            .filter(|c| matches!(c, Normal(_))) // prevent traversal attacks
348            .collect();
349        if entry.header().entry_type().is_file() {
350            entry.unpack_in(&dst_normalized)?;
351        } else if entry.header().entry_type() == EntryType::Directory {
352            std::fs::create_dir_all(Path::new(&dst_normalized).join(&path))?;
353        }
354    }
355    Ok(())
356}
357
358pub fn unpack_sans_parent<R>(mut archive: tar::Archive<R>, dst: &Path) -> std::io::Result<()>
359where
360    R: std::io::Read,
361{
362    use std::path::Component::Normal;
363
364    let dst_normalized = normalize_path(dst.to_string_lossy().as_ref());
365
366    for entry in archive.entries()? {
367        let mut entry = entry?;
368        let path: PathBuf = entry
369            .path()?
370            .components()
371            .skip(1) // strip top-level directory
372            .filter(|c| matches!(c, Normal(_))) // prevent traversal attacks
373            .collect();
374        entry.unpack(Path::new(&dst_normalized).join(path))?;
375    }
376    Ok(())
377}
378
379pub fn whoami(
380    wasmer_dir: &Path,
381    registry: Option<&str>,
382    token: Option<&str>,
383) -> Result<(String, String), anyhow::Error> {
384    use crate::graphql::queries::{who_am_i_query, WhoAmIQuery};
385
386    let config = WasmerConfig::from_file(wasmer_dir);
387
388    let config = config
389        .map_err(|e| anyhow::anyhow!("{e}"))
390        .with_context(|| format!("{registry:?}"))?;
391
392    let registry = match registry {
393        Some(s) => format_graphql(s),
394        None => config.registry.get_current_registry(),
395    };
396
397    let login_token = token
398        .map(|s| s.to_string())
399        .or_else(|| config.registry.get_login_token_for_registry(&registry))
400        .ok_or_else(|| anyhow::anyhow!("not logged into registry {:?}", registry))?;
401
402    let q = WhoAmIQuery::build_query(who_am_i_query::Variables {});
403    let response: who_am_i_query::ResponseData =
404        crate::graphql::execute_query(&registry, &login_token, &q)
405            .with_context(|| format!("{registry:?}"))?;
406
407    let username = response
408        .viewer
409        .as_ref()
410        .ok_or_else(|| anyhow::anyhow!("not logged into registry {:?}", registry))?
411        .username
412        .to_string();
413
414    Ok((registry, username))
415}
416
417pub fn test_if_registry_present(registry: &str) -> Result<bool, String> {
418    use crate::graphql::queries::{test_if_registry_present, TestIfRegistryPresent};
419
420    let q = TestIfRegistryPresent::build_query(test_if_registry_present::Variables {});
421    crate::graphql::execute_query_modifier_inner_check_json(
422        registry,
423        "",
424        &q,
425        Some(Duration::from_secs(1)),
426        |f| f,
427    )
428    .map_err(|e| format!("{e}"))?;
429
430    Ok(true)
431}
432
433pub fn get_all_available_registries(wasmer_dir: &Path) -> Result<Vec<String>, String> {
434    let config = WasmerConfig::from_file(wasmer_dir)?;
435    let mut registries = Vec::new();
436    for login in config.registry.tokens {
437        registries.push(format_graphql(&login.registry));
438    }
439    Ok(registries)
440}
441
442/// A library that exposes bindings to a Wasmer package.
443#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct Bindings {
445    /// A unique ID specifying this set of bindings.
446    pub id: String,
447    /// The URL which can be used to download the files that were generated
448    /// (typically as a `*.tar.gz` file).
449    pub url: String,
450    /// The programming language these bindings are written in.
451    pub language: ProgrammingLanguage,
452    /// The generator used to generate these bindings.
453    pub generator: BindingsGenerator,
454}
455
456/// The generator used to create [`Bindings`].
457#[derive(Debug, Clone, PartialEq, Eq)]
458pub struct BindingsGenerator {
459    /// A unique ID specifying this generator.
460    pub id: String,
461    /// The generator package's name (e.g. `wasmer/wasmer-pack`).
462    pub package_name: String,
463    /// The exact package version.
464    pub version: String,
465    /// The name of the command that was used for generating bindings.
466    pub command: String,
467}
468
469impl fmt::Display for BindingsGenerator {
470    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471        let BindingsGenerator {
472            package_name,
473            version,
474            command,
475            ..
476        } = self;
477
478        write!(f, "{package_name}@{version}:{command}")?;
479
480        Ok(())
481    }
482}
483
484/// List all bindings associated with a particular package.
485///
486/// If a version number isn't provided, this will default to the most recently
487/// published version.
488pub fn list_bindings(
489    registry: &str,
490    name: &str,
491    version: Option<&str>,
492) -> Result<Vec<Bindings>, anyhow::Error> {
493    use crate::graphql::queries::{
494        get_bindings_query::{ResponseData, Variables},
495        GetBindingsQuery,
496    };
497
498    let variables = Variables {
499        name: name.to_string(),
500        version: version.map(String::from),
501    };
502
503    let q = GetBindingsQuery::build_query(variables);
504    let response: ResponseData = crate::graphql::execute_query(registry, "", &q)?;
505
506    let package_version = response.package_version.context("Package not found")?;
507
508    let mut bindings_packages = Vec::new();
509
510    for b in package_version.bindings.into_iter().flatten() {
511        let pkg = Bindings {
512            id: b.id,
513            url: b.url,
514            language: b.language,
515            generator: BindingsGenerator {
516                id: b.generator.package_version.id,
517                package_name: b.generator.package_version.package.name,
518                version: b.generator.package_version.version,
519                command: b.generator.command_name,
520            },
521        };
522        bindings_packages.push(pkg);
523    }
524
525    Ok(bindings_packages)
526}