use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Display;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, bail, Context, Result};
use cargo_toml::{Manifest, Product};
use config::Config;
use semver::{Version, VersionReq};
use serde::{Deserialize, Deserializer};
use tracing::{trace, warn};
use url::Url;
use wadm_types::{Component, Properties, SecretSourceProperty};
use wasm_pkg_client::{CustomConfig, Registry, RegistryMapping, RegistryMetadata};
use wasm_pkg_core::config::{Config as PackageConfig, Override};
use wasmcloud_control_interface::RegistryCredential;
use wasmcloud_core::{parse_wit_package_name, WitFunction, WitInterface, WitNamespace, WitPackage};
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum LanguageConfig {
Rust(RustConfig),
TinyGo(TinyGoConfig),
Go(GoConfig),
Other(String),
}
#[allow(clippy::large_enum_variant)]
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TypeConfig {
#[serde(alias = "component")]
Component(ComponentConfig),
Provider(ProviderConfig),
}
impl TypeConfig {
pub fn wit_world(&self) -> &Option<String> {
match self {
TypeConfig::Component(c) => &c.wit_world,
TypeConfig::Provider(c) => &c.wit_world,
}
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct ComponentConfig {
#[serde(default = "default_key_directory")]
pub key_directory: PathBuf,
#[serde(default, deserialize_with = "wasm_target")]
pub wasm_target: WasmTarget,
pub wasip1_adapter_path: Option<PathBuf>,
pub wit_world: Option<String>,
pub tags: Option<HashSet<String>>,
pub build_artifact: Option<PathBuf>,
pub build_command: Option<String>,
pub destination: Option<PathBuf>,
}
fn wasm_target<'de, D>(target: D) -> Result<WasmTarget, D::Error>
where
D: Deserializer<'de>,
{
let target = String::deserialize(target)?;
Ok(target.as_str().into())
}
impl RustConfig {
#[must_use]
pub fn build_target(&self, wasm_target: &WasmTarget) -> &'static str {
match wasm_target {
WasmTarget::CoreModule => "wasm32-unknown-unknown",
WasmTarget::WasiP1 => "wasm32-wasip1",
WasmTarget::WasiP2 => "wasm32-wasip2",
}
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct ProviderConfig {
#[serde(default = "default_vendor")]
pub vendor: String,
pub wit_world: Option<String>,
#[serde(default = "default_os")]
pub os: String,
#[serde(default = "default_arch")]
pub arch: String,
pub rust_target: Option<String>,
pub bin_name: Option<String>,
#[serde(default = "default_key_directory")]
pub key_directory: PathBuf,
}
fn default_vendor() -> String {
"NoVendor".to_string()
}
fn default_os() -> String {
std::env::consts::OS.to_string()
}
fn default_arch() -> String {
std::env::consts::ARCH.to_string()
}
fn default_key_directory() -> PathBuf {
let home_dir = etcetera::home_dir().unwrap();
home_dir.join(".wash/keys")
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct RustConfig {
pub cargo_path: Option<PathBuf>,
pub target_path: Option<PathBuf>,
#[serde(default)]
pub debug: bool,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct RegistryConfig {
#[serde(flatten)]
pub push: RegistryPushConfig,
pub pull: Option<RegistryPullConfig>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct RegistryPushConfig {
pub url: Option<String>,
pub credentials: Option<PathBuf>,
#[serde(default)]
pub push_insecure: bool,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
pub struct RegistryPullConfig {
pub sources: Vec<RegistryPullSourceOverride>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
pub struct RegistryPullSourceOverride {
pub target: String,
pub source: RegistryPullSource,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub enum RegistryPullSource {
#[default]
Builtin,
LocalPath(String),
RemoteHttpWellKnown(String),
RemoteOci(String),
RemoteHttp(String),
RemoteGit(String),
}
impl Display for RegistryPullSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegistryPullSource::Builtin => write!(f, "builtin")?,
RegistryPullSource::LocalPath(s)
| RegistryPullSource::RemoteHttpWellKnown(s)
| RegistryPullSource::RemoteOci(s)
| RegistryPullSource::RemoteHttp(s)
| RegistryPullSource::RemoteGit(s) => write!(f, "{}", s)?,
}
Ok(())
}
}
impl<'de> Deserialize<'de> for RegistryPullSource {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
impl TryFrom<String> for RegistryPullSource {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self> {
Self::from_str(&value)
}
}
impl FromStr for RegistryPullSource {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(match s {
s if s.starts_with("file://") => Self::LocalPath(s.into()),
s if s.starts_with("oci://") => Self::RemoteOci(s.into()),
s if s.starts_with("http://") || s.starts_with("https://") => {
Self::RemoteHttp(s.into())
}
s if s.starts_with("git+ssh://")
|| s.starts_with("git+http://")
|| s.starts_with("git+https://") =>
{
Self::RemoteGit(s.into())
}
"builtin" => Self::Builtin,
s => bail!("unrecognized registry pull source [{s}]"),
})
}
}
impl RegistryPullSource {
pub async fn resolve_file_path(&self, base_dir: impl AsRef<Path>) -> Result<PathBuf> {
match self {
RegistryPullSource::LocalPath(p) => match p.strip_prefix("file://") {
Some(s) if s.starts_with("/") => tokio::fs::canonicalize(s)
.await
.with_context(|| format!("failed to canonicalize absolute path [{s}]")),
Some(s) => tokio::fs::canonicalize(base_dir.as_ref().join(s))
.await
.with_context(|| format!("failed to canonicalize relative path [{s}]")),
None => bail!("invalid RegistryPullSource file path [{p}]"),
},
_ => bail!("registry pull source does not resolve to file path"),
}
}
}
impl TryFrom<RegistryPullSource> for RegistryMapping {
type Error = anyhow::Error;
fn try_from(value: RegistryPullSource) -> Result<Self> {
match value {
RegistryPullSource::Builtin | RegistryPullSource::LocalPath(_) => {
bail!("builtins and local files cannot be converted to registry mappings")
}
RegistryPullSource::RemoteHttp(_) => {
bail!("remote files HTTP files cannot be converted to registry mappings")
}
RegistryPullSource::RemoteGit(_) => {
bail!("remote git repositories files cannot be converted to registry mappings")
}
RegistryPullSource::RemoteHttpWellKnown(url) => {
let url = Url::parse(&url).context("failed to parse url")?;
Registry::from_str(url.as_str())
.map(RegistryMapping::Registry)
.map_err(|e| anyhow!(e))
}
RegistryPullSource::RemoteOci(uri) => {
let url = Url::parse(&uri).context("failed to parse url")?;
if url.scheme() != "oci" {
bail!("invalid scheme [{}], expected 'oci'", url.scheme());
}
let metadata = {
let mut metadata = RegistryMetadata::default();
metadata.preferred_protocol = Some("oci".into());
let mut protocol_configs = serde_json::Map::new();
let namespace_prefix = format!(
"{}/",
url.path().strip_prefix('/').unwrap_or_else(|| url.path())
);
protocol_configs.insert(
"namespacePrefix".into(),
serde_json::json!(namespace_prefix),
);
metadata.protocol_configs = HashMap::from([("oci".into(), protocol_configs)]);
metadata
};
Ok(RegistryMapping::Custom(CustomConfig {
registry: Registry::from_str(&format!(
"{}{}",
url.authority(),
url.port().map(|p| format!(":{p}")).unwrap_or_default()
))
.map_err(|e| anyhow!(e))?,
metadata,
}))
}
}
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct CommonConfig {
pub name: String,
pub version: Version,
pub revision: i32,
pub project_dir: PathBuf,
pub build_dir: PathBuf,
pub wit_dir: PathBuf,
pub wasm_bin_name: Option<String>,
pub registry: RegistryConfig,
}
impl CommonConfig {
#[must_use]
pub fn wasm_bin_name(&self) -> String {
self.wasm_bin_name
.clone()
.unwrap_or_else(|| self.name.clone())
}
}
#[derive(Debug, Deserialize, Default, Clone, Eq, PartialEq)]
pub enum WasmTarget {
#[default]
#[serde(alias = "wasm32-unknown-unknown")]
CoreModule,
#[serde(
alias = "wasm32-wasi",
alias = "wasm32-wasi-preview1",
alias = "wasm32-wasip1"
)]
WasiP1,
#[serde(
alias = "wasm32-wasip2",
alias = "wasm32-wasi-preview2",
alias = "wasm32-preview2"
)]
WasiP2,
}
impl From<&str> for WasmTarget {
fn from(value: &str) -> Self {
match value {
"wasm32-wasi-preview1" => WasmTarget::WasiP1,
"wasm32-wasip1" => WasmTarget::WasiP1,
"wasm32-wasi" => WasmTarget::WasiP1,
"wasm32-wasi-preview2" => WasmTarget::WasiP2,
"wasm32-wasip2" => WasmTarget::WasiP2,
"wasm32-unknown-unknown" => WasmTarget::CoreModule,
_ => {
warn!("Unknown wasm_target `{value}`, expected wasm32-wasip2 or wasm32-wasip1. Defaulting to wasm32-unknown-unknown");
WasmTarget::CoreModule
}
}
}
}
impl From<String> for WasmTarget {
fn from(value: String) -> Self {
value.as_str().into()
}
}
impl Display for WasmTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match &self {
WasmTarget::CoreModule => "wasm32-unknown-unknown",
WasmTarget::WasiP1 => "wasm32-wasip1",
WasmTarget::WasiP2 => "wasm32-wasip2",
})
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct GoConfig {
pub go_path: Option<PathBuf>,
#[serde(default)]
pub disable_go_generate: bool,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TinyGoScheduler {
None,
Tasks,
Asyncify,
}
impl TinyGoScheduler {
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Tasks => "tasks",
Self::Asyncify => "asyncify",
}
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TinyGoGarbageCollector {
None,
Conservative,
Leaking,
}
impl TinyGoGarbageCollector {
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Conservative => "conservative",
Self::Leaking => "leaking",
}
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct TinyGoConfig {
pub tinygo_path: Option<PathBuf>,
#[serde(default)]
pub disable_go_generate: bool,
pub scheduler: Option<TinyGoScheduler>,
pub garbage_collector: Option<TinyGoGarbageCollector>,
}
impl TinyGoConfig {
#[must_use]
pub fn build_target(&self, wasm_target: &WasmTarget) -> &'static str {
match wasm_target {
WasmTarget::CoreModule => "wasm",
WasmTarget::WasiP1 => "wasi",
WasmTarget::WasiP2 => "wasip2",
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged)]
pub enum DevConfigSpec {
Named { name: String },
Values { values: BTreeMap<String, String> },
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged)]
pub enum DevSecretSpec {
Existing {
name: String,
source: SecretSourceProperty,
},
Values {
name: String,
values: BTreeMap<String, String>,
},
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
pub struct DevManifestComponentTarget {
pub component_name: Option<String>,
pub component_id: Option<String>,
pub component_ref: Option<String>,
pub path: PathBuf,
}
impl DevManifestComponentTarget {
pub fn matches(&self, component: &Component) -> bool {
let (component_id, component_ref) = match &component.properties {
Properties::Component { ref properties } => (&properties.id, &properties.image),
Properties::Capability { ref properties } => (&properties.id, &properties.image),
};
if self
.component_name
.as_ref()
.is_some_and(|v| v == &component.name)
{
return true;
}
if self
.component_id
.as_ref()
.is_some_and(|a| component_id.as_ref().is_some_and(|b| a == b))
{
return true;
}
if self
.component_ref
.as_ref()
.is_some_and(|v| component_ref.as_ref().is_some_and(|c| c == v))
{
return true;
}
false
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct InterfaceComponentOverride {
#[serde(alias = "interface")]
pub interface_spec: String,
pub config: Option<OneOrMore<DevConfigSpec>>,
pub secrets: Option<OneOrMore<DevSecretSpec>>,
#[serde(alias = "uri")]
pub image_ref: Option<String>,
pub link_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WitInterfaceSpec {
pub namespace: WitNamespace,
pub package: WitPackage,
pub interfaces: Option<HashSet<WitInterface>>,
pub function: Option<WitFunction>,
pub version: Option<Version>,
}
impl WitInterfaceSpec {
pub fn includes(&self, other: &Self) -> bool {
!self.is_disjoint(other)
}
pub fn is_disjoint(&self, other: &Self) -> bool {
if self.namespace != other.namespace {
return true;
}
if self.package != other.package {
return true;
}
match (self.interfaces.as_ref(), other.interfaces.as_ref()) {
(None, None) |
(Some(_), None) |
(None, Some(_)) => {
return false;
}
(Some(iface), Some(other_iface)) if iface != other_iface => {
return true;
}
(Some(_), Some(_)) => {}
}
match (self.function.as_ref(), other.function.as_ref()) {
(None, None) |
(Some(_), None) |
(None, Some(_)) => {
return false;
}
(Some(f), Some(other_f)) if f != other_f => {
return true;
}
(Some(_), Some(_)) => {}
}
match (self.version.as_ref(), other.version.as_ref()) {
(None, None) |
(Some(_), None) |
(None, Some(_)) => {
false
}
(Some(v), Some(other_v)) if VersionReq::parse(&format!("^{v}")).is_ok_and(|req| req.matches(other_v)) => { false }
(Some(v), Some(other_v)) if VersionReq::parse(&format!("^{other_v}")).is_ok_and(|req| req.matches(v)) => {
false
}
_ => true
}
}
}
impl std::str::FromStr for WitInterfaceSpec {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match parse_wit_package_name(s) {
Ok((namespace, packages, interfaces, function, version))
if packages.len() == 1
&& (interfaces.is_none()
|| interfaces.as_ref().is_some_and(|v| v.len() == 1)) =>
{
Ok(Self {
namespace,
package: packages
.into_iter()
.next()
.context("unexpectedly missing package")?,
interfaces: match interfaces {
Some(v) if v.is_empty() => bail!("unexpectedly missing interface"),
Some(v) => Some(v.into_iter().collect()),
None => None,
},
function,
version,
})
}
Ok((_, _, _, Some(_), _)) => {
bail!("function-level interface overrides are not yet supported")
}
Ok(_) => bail!("nested interfaces not yet supported"),
Err(e) => bail!("failed to parse WIT interface spec (\"{s}\"): {e}"),
}
}
}
impl<'de> Deserialize<'de> for WitInterfaceSpec {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Multi {
Stringified(String),
Explicit {
namespace: String,
package: String,
interface: Option<String>,
function: Option<String>,
version: Option<Version>,
},
}
match Multi::deserialize(deserializer)? {
Multi::Stringified(s) => Self::from_str(&s).map_err(|e| {
serde::de::Error::custom(format!(
"failed to parse WIT interface specification: {e}"
))
}),
Multi::Explicit {
namespace,
package,
interface,
function,
version,
} => Ok(Self {
namespace,
package,
interfaces: interface.map(|i| HashSet::from([i])),
function,
version,
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub enum OneOrMore<T> {
One(T),
More(Vec<T>),
}
impl<T> OneOrMore<T> {
#[allow(unused)]
fn into_vec(self) -> Vec<T> {
match self {
OneOrMore::One(t) => vec![t],
OneOrMore::More(ts) => ts,
}
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
OneOrMoreIterator {
inner: self,
idx: 0,
}
}
}
pub struct OneOrMoreIterator<'a, T> {
inner: &'a OneOrMore<T>,
idx: usize,
}
impl<'a, T> Iterator for OneOrMoreIterator<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
match (self.idx, self.inner) {
(0, OneOrMore::One(inner)) => {
if let Some(v) = self.idx.checked_add(1) {
self.idx = v
}
Some(inner)
}
(_, OneOrMore::One(_)) => None,
(idx, OneOrMore::More(vs)) => {
if let Some(v) = self.idx.checked_add(1) {
self.idx = v
}
vs.get(idx)
}
}
}
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
pub struct InterfaceOverrides {
#[serde(default)]
pub imports: Vec<InterfaceComponentOverride>,
#[serde(default)]
pub exports: Vec<InterfaceComponentOverride>,
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
pub struct DevConfig {
#[serde(default)]
pub manifests: Vec<DevManifestComponentTarget>,
#[serde(default, alias = "configs")]
pub config: Vec<DevConfigSpec>,
#[serde(default)]
pub secrets: Vec<DevSecretSpec>,
#[serde(default)]
pub overrides: InterfaceOverrides,
}
pub async fn load_config(
opt_path: Option<PathBuf>,
use_env: Option<bool>,
) -> Result<ProjectConfig> {
let project_dir = match opt_path.clone() {
Some(p) => p,
None => std::env::current_dir().context("failed to get current directory")?,
};
let path = if !project_dir.exists() {
bail!("path {} does not exist", project_dir.display());
} else {
fs::canonicalize(&project_dir).context("failed to canonicalize project path")?
};
let (wasmcloud_toml_dir, wasmcloud_toml_path) = if path.is_dir() {
let wasmcloud_path = path.join("wasmcloud.toml");
if !wasmcloud_path.is_file() {
bail!("failed to find wasmcloud.toml in [{}]", path.display());
}
(path, wasmcloud_path)
} else if path.is_file() {
(
path.parent()
.ok_or_else(|| anyhow!("Could not get parent path of wasmcloud.toml file"))?
.to_path_buf(),
path,
)
} else {
bail!(
"failed to find wasmcloud.toml: path [{}] is not a directory or file",
path.display()
);
};
let mut config = Config::builder().add_source(config::File::from(wasmcloud_toml_path.clone()));
if use_env.unwrap_or(true) {
config = config.add_source(config::Environment::with_prefix("WASMCLOUD"));
}
let json_value = config
.build()
.map_err(|e| {
if e.to_string().contains("is not of a registered file format") {
return anyhow!("invalid config file: {}", wasmcloud_toml_path.display());
}
anyhow!("{}", e)
})?
.try_deserialize::<serde_json::Value>()?;
let mut toml_project_config: WasmcloudDotToml = serde_json::from_value(json_value)?;
let current_config = toml_project_config
.package_config
.take()
.unwrap_or_default();
if current_config != PackageConfig::default() {
toml_project_config.package_config = Some(current_config);
}
if toml_project_config.package_config.is_none() {
let wkg_toml_path = wasmcloud_toml_dir.join(wasm_pkg_core::config::CONFIG_FILE_NAME);
match tokio::fs::metadata(&wkg_toml_path).await {
Ok(meta) if meta.is_file() => {
match PackageConfig::load_from_path(wkg_toml_path).await {
Ok(wkg_config) => {
toml_project_config.package_config = Some(wkg_config);
}
Err(e) => {
tracing::warn!(err = %e, "failed to load wkg.toml");
}
}
}
Ok(_) => (),
Err(e) => {
if e.kind() != std::io::ErrorKind::NotFound {
tracing::warn!(err = %e, "IO error when trying to fallback to wkg.toml");
}
}
};
}
toml_project_config
.convert(wasmcloud_toml_dir)
.map_err(|e: anyhow::Error| anyhow!("{} in {}", e, wasmcloud_toml_path.display()))
}
#[derive(Deserialize, Debug)]
pub struct WasmcloudDotToml {
pub language: String,
#[serde(rename = "type")]
pub project_type: String,
pub name: Option<String>,
pub version: Option<Version>,
#[serde(default)]
pub revision: i32,
pub path: Option<PathBuf>,
pub wit: Option<PathBuf>,
pub build: Option<PathBuf>,
#[serde(default)]
pub component: ComponentConfig,
#[serde(default)]
pub provider: ProviderConfig,
#[serde(default)]
pub rust: RustConfig,
#[serde(default)]
pub tinygo: TinyGoConfig,
#[serde(default)]
pub go: GoConfig,
#[serde(default)]
pub dev: DevConfig,
#[serde(flatten)]
pub package_config: Option<PackageConfig>,
#[serde(default)]
pub registry: RegistryConfig,
}
impl WasmcloudDotToml {
fn build_common_config_from_cargo_project(
project_dir: PathBuf,
build_dir: PathBuf,
wit_dir: PathBuf,
name: Option<String>,
version: Option<Version>,
revision: i32,
registry: RegistryConfig,
) -> Result<CommonConfig> {
let cargo_toml_path = project_dir.join("Cargo.toml");
if !cargo_toml_path.is_file() {
bail!(
"missing/invalid Cargo.toml path [{}]",
cargo_toml_path.display(),
);
}
let mut cargo_toml = Manifest::from_path(cargo_toml_path)?;
cargo_toml.complete_from_path(&project_dir)?;
let cargo_pkg = cargo_toml
.package
.ok_or_else(|| anyhow!("Missing package information in Cargo.toml"))?;
let version = match version {
Some(version) => version,
None => Version::parse(cargo_pkg.version.get()?.as_str())?,
};
let name = name.unwrap_or(cargo_pkg.name);
let wasm_bin_name = match cargo_toml.lib {
Some(Product {
name: Some(lib_name),
..
}) => Some(lib_name),
_ => None,
};
Ok(CommonConfig {
name,
version,
revision,
wit_dir,
build_dir,
project_dir,
wasm_bin_name,
registry,
})
}
pub fn convert(self, wasmcloud_toml_dir: PathBuf) -> Result<ProjectConfig> {
let project_type_config = match self.project_type.trim().to_lowercase().as_str() {
"component" => TypeConfig::Component(self.component),
"provider" => TypeConfig::Provider(self.provider),
project_type => bail!("unknown project type: {project_type}"),
};
let language_config = match self.language.trim().to_lowercase().as_str() {
"rust" => LanguageConfig::Rust(self.rust),
"go" => LanguageConfig::Go(self.go),
"tinygo" => LanguageConfig::TinyGo(self.tinygo),
other => LanguageConfig::Other(other.to_string()),
};
let project_path = self
.path
.map(|p| {
if p.is_absolute() {
p
} else {
wasmcloud_toml_dir.join(p)
}
})
.unwrap_or_else(|| wasmcloud_toml_dir.clone());
let project_path = project_path.canonicalize().with_context(|| {
format!(
"failed to canonicalize project path, ensure it exists: [{}]",
project_path.display()
)
})?;
let build_dir = self
.build
.map(|build_dir| {
if build_dir.is_absolute() {
Ok(build_dir)
} else {
canonicalize_or_create(wasmcloud_toml_dir.join(build_dir.as_path()))
}
})
.unwrap_or_else(|| Ok(project_path.join("build")))?;
let wit_dir = self
.wit
.map(|wit_dir| {
if wit_dir.is_absolute() {
Ok(wit_dir)
} else {
wasmcloud_toml_dir
.join(wit_dir.as_path())
.canonicalize()
.with_context(|| {
format!(
"failed to canonicalize wit directory, ensure it exists: [{}]",
wit_dir.display()
)
})
}
})
.unwrap_or_else(|| Ok(project_path.join("wit")))?;
let common_config = match language_config {
LanguageConfig::Rust(_) => {
match Self::build_common_config_from_cargo_project(
project_path.clone(),
build_dir.clone(),
wit_dir.clone(),
self.name.clone(),
self.version.clone(),
self.revision,
self.registry.clone(),
) {
Ok(cfg) => cfg,
Err(_) if self.name.is_some() && self.version.is_some() => CommonConfig {
name: self.name.unwrap(),
version: self.version.unwrap(),
revision: self.revision,
wasm_bin_name: None,
project_dir: project_path,
wit_dir,
build_dir,
registry: self.registry,
},
Err(err) => {
bail!("No Cargo.toml file found in the current directory, and name/version unspecified: {err}")
}
}
}
LanguageConfig::Go(_) | LanguageConfig::TinyGo(_) | LanguageConfig::Other(_) => {
CommonConfig {
name: self
.name
.ok_or_else(|| anyhow!("Missing name in wasmcloud.toml"))?,
version: self
.version
.ok_or_else(|| anyhow!("Missing version in wasmcloud.toml"))?,
revision: self.revision,
project_dir: project_path,
wasm_bin_name: None,
wit_dir,
build_dir,
registry: self.registry,
}
}
};
let package_config = self
.package_config
.map(|mut package_config| {
package_config.overrides = package_config.overrides.map(|overrides| {
overrides
.into_iter()
.map(|(k, mut v)| {
if let Some(path) = v.path.as_ref() {
trace!("canonicalizing override path: [{}]", path.display());
let path = if path.is_absolute() {
path.clone()
} else {
let override_path = wasmcloud_toml_dir.join(path);
override_path.canonicalize().unwrap_or_else(|e| {
warn!(
?e,
"failed to canonicalize override path, falling back to: [{}]",
override_path.display()
);
override_path
})
};
v.path = Some(path);
}
(k, v)
})
.collect::<HashMap<String, Override>>()
});
package_config
})
.unwrap_or_default();
Ok(ProjectConfig {
dev: self.dev,
project_type: project_type_config,
language: language_config,
common: common_config,
package_config,
wasmcloud_toml_dir,
})
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct ProjectConfig {
pub language: LanguageConfig,
#[serde(rename = "type")]
pub project_type: TypeConfig,
pub common: CommonConfig,
pub dev: DevConfig,
pub package_config: PackageConfig,
#[serde(skip)]
pub wasmcloud_toml_dir: PathBuf,
}
impl ProjectConfig {
pub fn resolve_registry_credentials(
&self,
registry: impl AsRef<str>,
) -> Result<RegistryCredential> {
let credentials_file = &self.common.registry.push.credentials.clone();
let Some(credentials_file) = credentials_file else {
bail!("No registry credentials path configured")
};
if !credentials_file.exists() {
bail!(
"Provided registry credentials file ({}) does not exist",
credentials_file.display()
)
}
let credentials = std::fs::read_to_string(credentials_file).with_context(|| {
format!(
"Failed to read registry credentials file {}",
credentials_file.display()
)
})?;
let credentials = serde_json::from_str::<HashMap<String, RegistryCredential>>(&credentials)
.with_context(|| {
format!(
"Failed to parse registry credentials from file {}",
credentials_file.display()
)
})?;
let Some(credentials) = credentials.get(registry.as_ref()) else {
bail!(
"Unable to find credentials for {} in the configured registry credentials file",
registry.as_ref()
)
};
Ok(credentials.clone())
}
}
fn canonicalize_or_create(path: PathBuf) -> Result<PathBuf> {
match path.canonicalize() {
Ok(path) => Ok(path),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir_all(&path).with_context(|| {
format!(
"failed to create directory [{}] before canonicalizing",
path.display()
)
})?;
path.canonicalize().with_context(|| {
format!(
"failed to canonicalize directory [{}] after creating it",
path.display()
)
})
}
Err(e) => {
Err(e).with_context(|| format!("failed to canonicalize directory [{}]", path.display()))
}
}
}
#[cfg(test)]
mod tests {
use crate::parser::WitInterfaceSpec;
use std::str::FromStr;
#[test]
fn test_includes() {
let wasi_http = WitInterfaceSpec::from_str("wasi:http")
.expect("should parse 'wasi:http' into WitInterfaceSpec");
let wasi_http_incoming_handler = WitInterfaceSpec::from_str("wasi:http/incoming-handler")
.expect("should parse 'wasi:http/incoming-handler' into WitInterfaceSpec");
let wasi_http_incoming_handler_handle =
WitInterfaceSpec::from_str("wasi:http/incoming-handler.handle")
.expect("should parse 'wasi:http/incoming-handler.handle' into WitInterfaceSpec");
assert!(wasi_http.includes(&wasi_http_incoming_handler));
assert!(wasi_http_incoming_handler.includes(&wasi_http_incoming_handler_handle));
}
}