pub mod package_wizard;
pub mod prompts;
pub mod render;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use anyhow::Context;
use once_cell::sync::Lazy;
use regex::Regex;
use wasmer_api::backend::BackendClient;
use wasmer_deploy_schema::schema::StringWebcIdent;
pub const DEFAULT_PACKAGE_MANIFEST_FILE: &str = "wasmer.toml";
#[derive(Debug)]
pub struct Labeled<T> {
pub label: String,
pub value: T,
}
pub fn load_package_manifest(
path: &Path,
) -> Result<Option<(PathBuf, wasmer_toml::Manifest)>, anyhow::Error> {
let file_path = if path.is_file() {
path.to_owned()
} else {
path.join(DEFAULT_PACKAGE_MANIFEST_FILE)
};
let contents = match std::fs::read_to_string(&file_path) {
Ok(c) => c,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(err).with_context(|| {
format!(
"Could not read package manifest at '{}'",
file_path.display()
)
})
}
};
let manifest = wasmer_toml::Manifest::parse(&contents).with_context(|| {
format!(
"Could not parse package config at: '{}'",
file_path.display()
)
})?;
Ok(Some((file_path, manifest)))
}
pub fn prompt_for_package_name(
message: &str,
default: Option<&str>,
) -> Result<StringWebcIdent, anyhow::Error> {
loop {
let raw: String = dialoguer::Input::new()
.with_prompt(message)
.with_initial_text(default.unwrap_or_default())
.interact_text()
.context("could not read user input")?;
match raw.parse::<StringWebcIdent>() {
Ok(p) => break Ok(p),
Err(err) => {
eprintln!("invalid package name: {err}");
}
}
}
}
pub enum PackageCheckMode {
MustExist,
MustNotExist,
}
pub async fn prompt_for_package(
message: &str,
default: Option<&str>,
check: Option<PackageCheckMode>,
client: Option<&BackendClient>,
) -> Result<(StringWebcIdent, Option<wasmer_api::backend::gql::Package>), anyhow::Error> {
loop {
let name = prompt_for_package_name(message, default)?;
if let Some(check) = &check {
let api = client.expect("Check mode specified, but no API provided");
let pkg = wasmer_api::backend::get_package(api, name.to_string())
.await
.context("could not query backend for package")?;
match check {
PackageCheckMode::MustExist => {
if let Some(pkg) = pkg {
break Ok((name, Some(pkg)));
} else {
eprintln!("Package '{name}' does not exist");
}
}
PackageCheckMode::MustNotExist => {
if pkg.is_none() {
break Ok((name, None));
} else {
eprintln!("Package '{name}' already exists");
}
}
}
}
}
}
pub async fn republish_package_with_bumped_version(
client: &BackendClient,
manifest_path: &Path,
mut manifest: wasmer_toml::Manifest,
) -> Result<wasmer_toml::Manifest, anyhow::Error> {
let current_opt = wasmer_api::backend::get_package(client, manifest.package.name.clone())
.await
.context("could not load package info from backend")?
.and_then(|x| x.last_version);
let new_version = if let Some(current) = current_opt {
let mut v = semver::Version::parse(¤t.version)
.with_context(|| format!("Could not parse package version: '{}'", current.version))?;
v.patch += 1;
v
} else {
manifest.package.version
};
manifest.package.version = new_version;
let contents = toml::to_string(&manifest).with_context(|| {
format!(
"could not persist manifest to '{}'",
manifest_path.display()
)
})?;
std::fs::write(manifest_path, contents)
.with_context(|| format!("could not write manifest to '{}'", manifest_path.display()))?;
let dir = manifest_path
.parent()
.context("could not determine wasmer.toml parent directory")?
.to_owned();
let registry = client.graphql_endpoint().to_string();
let token = client.auth_token().map(|s| s.to_string());
std::thread::spawn({
move || {
let publish = wasmer_registry::package::builder::Publish {
registry: Some(registry),
dry_run: false,
quiet: false,
package_name: None,
version: None,
token,
no_validate: true,
package_path: Some(dir.to_str().unwrap().to_string()),
};
publish.execute()
}
})
.join()
.expect("thread failed")?;
Ok(manifest)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Identifier {
pub name: String,
pub owner: Option<String>,
pub version: Option<String>,
}
impl Identifier {
pub fn new(name: impl Into<String>) -> Self {
Identifier {
name: name.into(),
owner: None,
version: None,
}
}
pub fn with_owner(self, owner: impl Into<String>) -> Self {
Identifier {
owner: Some(owner.into()),
..self
}
}
pub fn with_version(self, version: impl Into<String>) -> Self {
Identifier {
version: Some(version.into()),
..self
}
}
}
impl FromStr for Identifier {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
const PATTERN: &str = r"^(?x)
(?:
(?P<owner>[a-zA-Z][\w\d_.-]*)
/
)?
(?P<name>[a-zA-Z][\w\d_.-]*)
(?:
@
(?P<version>[\w\d.]+)
)?
$
";
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(PATTERN).unwrap());
let caps = RE.captures(s).context(
"Invalid package identifier, expected something like namespace/package@version",
)?;
let mut identifier = Identifier::new(&caps["name"]);
if let Some(owner) = caps.name("owner") {
identifier = identifier.with_owner(owner.as_str());
}
if let Some(version) = caps.name("version") {
identifier = identifier.with_version(version.as_str());
}
Ok(identifier)
}
}
pub(crate) fn merge_yaml_values(a: &serde_yaml::Value, b: &serde_yaml::Value) -> serde_yaml::Value {
use serde_yaml::Value as V;
match (a, b) {
(V::Mapping(a), V::Mapping(b)) => {
let mut m = a.clone();
for (k, v) in b.iter() {
let newval = if let Some(old) = a.get(k) {
merge_yaml_values(old, v)
} else {
v.clone()
};
m.insert(k.clone(), newval);
}
V::Mapping(m)
}
_ => b.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_yaml_values() {
use serde_yaml::Value;
let v1 = r#"
a: a
b:
b1: b1
c: c
"#;
let v2 = r#"
a: a1
b:
b2: b2
"#;
let v3 = r#"
a: a1
b:
b1: b1
b2: b2
c: c
"#;
let a: Value = serde_yaml::from_str(v1).unwrap();
let b: Value = serde_yaml::from_str(v2).unwrap();
let c: Value = serde_yaml::from_str(v3).unwrap();
let merged = merge_yaml_values(&a, &b);
assert_eq!(merged, c);
}
#[test]
fn parse_valid_identifiers() {
let inputs = [
("python", Identifier::new("python")),
(
"syrusakbary/python",
Identifier::new("python").with_owner("syrusakbary"),
),
(
"wasmer/wasmer.io",
Identifier::new("wasmer.io").with_owner("wasmer"),
),
(
"syrusakbary/python@1.2.3",
Identifier::new("python")
.with_owner("syrusakbary")
.with_version("1.2.3"),
),
(
"python@1.2.3",
Identifier::new("python").with_version("1.2.3"),
),
];
for (src, expected) in inputs {
let identifier = Identifier::from_str(src).expect(src);
assert_eq!(identifier, expected);
}
}
#[test]
fn invalid_package_identifiers() {
let inputs = ["", "$", "python/", "/python", "python@", "."];
for input in inputs {
let result = Identifier::from_str(input);
assert!(result.is_err(), "Got {result:?} from {input:?}");
}
}
}