wash_lib/parser/
mod.rs

1//! Parse wasmcloud.toml files which specify key information for building and signing
2//! WebAssembly modules and native capability provider binaries
3
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::fmt::Display;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::str::FromStr;
9
10use anyhow::{anyhow, bail, Context, Result};
11use cargo_toml::{Manifest, Product};
12use config::Config;
13use semver::{Version, VersionReq};
14use serde::{Deserialize, Deserializer};
15use tracing::{trace, warn};
16use url::Url;
17use wadm_types::{Component, Properties, SecretSourceProperty};
18use wasm_pkg_client::{CustomConfig, Registry, RegistryMapping, RegistryMetadata};
19use wasm_pkg_core::config::{Config as PackageConfig, Override};
20use wasmcloud_control_interface::RegistryCredential;
21use wasmcloud_core::{parse_wit_package_name, WitFunction, WitInterface, WitNamespace, WitPackage};
22
23#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
24#[serde(rename_all = "snake_case")]
25pub enum LanguageConfig {
26    Rust(RustConfig),
27    TinyGo(TinyGoConfig),
28    Go(GoConfig),
29    Other(String),
30}
31
32#[allow(clippy::large_enum_variant)]
33#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
34#[serde(rename_all = "snake_case")]
35pub enum TypeConfig {
36    #[serde(alias = "component")]
37    Component(ComponentConfig),
38    Provider(ProviderConfig),
39}
40
41impl TypeConfig {
42    pub fn wit_world(&self) -> &Option<String> {
43        match self {
44            TypeConfig::Component(c) => &c.wit_world,
45            TypeConfig::Provider(c) => &c.wit_world,
46        }
47    }
48}
49
50#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
51pub struct ComponentConfig {
52    /// The directory to store the private signing keys in.
53    #[serde(default = "default_key_directory")]
54    pub key_directory: PathBuf,
55    /// The target wasm target to build for. Defaults to "wasm32-unknown-unknown" (a WASM core module).
56    #[serde(default, deserialize_with = "wasm_target")]
57    pub wasm_target: WasmTarget,
58    /// Path to a wasm adapter that can be used for wasip2
59    pub wasip1_adapter_path: Option<PathBuf>,
60    /// The WIT world that is implemented by the component
61    pub wit_world: Option<String>,
62    /// Tags that should be applied during the component signing process
63    pub tags: Option<HashSet<String>>,
64    /// File path `wash` can use to find the built artifact. Defaults to `./build/[name].wasm`
65    pub build_artifact: Option<PathBuf>,
66    /// Optional build override command to run instead of attempting to use the native language
67    /// toolchain to build. Keep in mind that `wash` expects for the built artifact to be located
68    /// under the `build` directory of the project root unless overridden by `build_artifact`.
69    pub build_command: Option<String>,
70    /// File path the built and signed component should be written to. Defaults to `./build/[name]_s.wasm`
71    pub destination: Option<PathBuf>,
72}
73
74/// Custom deserializer to parse the wasm target string into a [`WasmTarget`] enum
75fn wasm_target<'de, D>(target: D) -> Result<WasmTarget, D::Error>
76where
77    D: Deserializer<'de>,
78{
79    let target = String::deserialize(target)?;
80    Ok(target.as_str().into())
81}
82
83impl RustConfig {
84    #[must_use]
85    pub fn build_target(&self, wasm_target: &WasmTarget) -> &'static str {
86        match wasm_target {
87            WasmTarget::CoreModule => "wasm32-unknown-unknown",
88            WasmTarget::WasiP1 => "wasm32-wasip1",
89            WasmTarget::WasiP2 => "wasm32-wasip2",
90        }
91    }
92}
93
94#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
95pub struct ProviderConfig {
96    /// The vendor name of the provider.
97    #[serde(default = "default_vendor")]
98    pub vendor: String,
99    /// Optional WIT world for the provider, e.g. `wasmcloud:messaging`
100    pub wit_world: Option<String>,
101    /// The target operating system of the provider archive. Defaults to the current OS.
102    #[serde(default = "default_os")]
103    pub os: String,
104    /// The target architecture of the provider archive. Defaults to the current architecture.
105    #[serde(default = "default_arch")]
106    pub arch: String,
107    /// The Rust target triple to build for. Defaults to the default rust toolchain.
108    pub rust_target: Option<String>,
109    /// Optional override for the provider binary name, required if we cannot infer this from Cargo.toml
110    pub bin_name: Option<String>,
111    /// The directory to store the private signing keys in.
112    #[serde(default = "default_key_directory")]
113    pub key_directory: PathBuf,
114}
115
116fn default_vendor() -> String {
117    "NoVendor".to_string()
118}
119fn default_os() -> String {
120    std::env::consts::OS.to_string()
121}
122fn default_arch() -> String {
123    std::env::consts::ARCH.to_string()
124}
125fn default_key_directory() -> PathBuf {
126    let home_dir = etcetera::home_dir().unwrap();
127    home_dir.join(".wash/keys")
128}
129
130#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
131pub struct RustConfig {
132    /// The path to the cargo binary. Optional, will default to search the user's `PATH` for `cargo` if not specified.
133    pub cargo_path: Option<PathBuf>,
134    /// Path to cargo/rust's `target` directory. Optional, defaults to the cargo target directory for the workspace or project.
135    pub target_path: Option<PathBuf>,
136    // Whether to build in debug mode. Defaults to false.
137    #[serde(default)]
138    pub debug: bool,
139}
140
141#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
142pub struct RegistryConfig {
143    /// Configuration to use when pushing this project to a registry
144    // NOTE: flattened for backwards compatibility
145    #[serde(flatten)]
146    pub push: RegistryPushConfig,
147
148    /// Configuration to use for pulling from registries
149    pub pull: Option<RegistryPullConfig>,
150}
151
152#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
153pub struct RegistryPushConfig {
154    /// URL of the registry to push to
155    pub url: Option<String>,
156
157    /// Credentials to use for the given registry
158    pub credentials: Option<PathBuf>,
159
160    /// Whether or not to push to the registry insecurely with http
161    #[serde(default)]
162    pub push_insecure: bool,
163}
164
165/// Configuration that governs pulling of packages from registries
166#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
167pub struct RegistryPullConfig {
168    /// List of sources that should be pulled
169    pub sources: Vec<RegistryPullSourceOverride>,
170}
171
172/// Information identifying a registry that can be pulled from
173#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
174pub struct RegistryPullSourceOverride {
175    /// Target specification for which this source applies (usually a namespace and/or package)
176    ///
177    /// ex. `wasi`, `wasi:keyvalue`, `wasi:keyvalue@0.2.0`
178    pub target: String,
179
180    /// The source for the configuration
181    pub source: RegistryPullSource,
182}
183
184/// Source for a registry pull
185#[derive(Debug, Default, PartialEq, Eq, Clone)]
186pub enum RegistryPullSource {
187    /// Sources for interfaces that are built-in/resolvable without configuration,
188    /// or special-cased in some other way
189    /// (e.g. `wasi:http` is a well known standard)
190    #[default]
191    Builtin,
192
193    /// A file source
194    ///
195    /// These references are resolved in two ways:
196    ///   - If a directory, then the namespace & path are appended
197    ///   - If a direct file then the file itself is used
198    ///
199    /// (ex. 'file://relative/path/to/file', 'file:///absolute/path/to/file')
200    LocalPath(String),
201
202    /// Remote HTTP registry, configured to support `.well-known/wasm-pkg/registry.json`
203    RemoteHttpWellKnown(String),
204
205    /// An OCI reference
206    ///
207    /// These references are resolved by appending the intended namespace and package
208    /// to the provided URI
209    ///
210    /// (ex. resolving `wasi:keyvalue@0.2.0` with 'oci://ghcr.io/wasmcloud/wit' becomes `oci://ghcr.io/wasmcloud/wit/wasi/keyvalue:0.2.0`)
211    RemoteOci(String),
212
213    /// URL to a HTTP/S resource
214    ///
215    /// These references are resolved by downloading and uncompressing (where possible) the linked file
216    /// as WIT, for whatever interfaces were provided.
217    ///
218    /// (ex. resolving `https://example.com/wit/package.tgz` means downloading and unpacking the tarball)
219    RemoteHttp(String),
220
221    /// URL to a GIT repository
222    ///
223    /// These URLs are guaranteed to start with a git-related scheme (ex. `git+http://`, `git+ssh://`, ...)
224    /// and will be used as the base under which to pull a folder of WIT
225    RemoteGit(String),
226}
227
228impl Display for RegistryPullSource {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        match self {
231            RegistryPullSource::Builtin => write!(f, "builtin")?,
232            RegistryPullSource::LocalPath(s)
233            | RegistryPullSource::RemoteHttpWellKnown(s)
234            | RegistryPullSource::RemoteOci(s)
235            | RegistryPullSource::RemoteHttp(s)
236            | RegistryPullSource::RemoteGit(s) => write!(f, "{}", s)?,
237        }
238        Ok(())
239    }
240}
241
242impl<'de> Deserialize<'de> for RegistryPullSource {
243    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
244    where
245        D: Deserializer<'de>,
246    {
247        Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
248    }
249}
250
251impl TryFrom<String> for RegistryPullSource {
252    type Error = anyhow::Error;
253
254    fn try_from(value: String) -> Result<Self> {
255        Self::from_str(&value)
256    }
257}
258
259impl FromStr for RegistryPullSource {
260    type Err = anyhow::Error;
261
262    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
263        Ok(match s {
264            s if s.starts_with("file://") => Self::LocalPath(s.into()),
265            s if s.starts_with("oci://") => Self::RemoteOci(s.into()),
266            s if s.starts_with("http://") || s.starts_with("https://") => {
267                Self::RemoteHttp(s.into())
268            }
269            s if s.starts_with("git+ssh://")
270                || s.starts_with("git+http://")
271                || s.starts_with("git+https://") =>
272            {
273                Self::RemoteGit(s.into())
274            }
275            "builtin" => Self::Builtin,
276            s => bail!("unrecognized registry pull source [{s}]"),
277        })
278    }
279}
280
281impl RegistryPullSource {
282    pub async fn resolve_file_path(&self, base_dir: impl AsRef<Path>) -> Result<PathBuf> {
283        match self {
284            RegistryPullSource::LocalPath(p) => match p.strip_prefix("file://") {
285                Some(s) if s.starts_with("/") => tokio::fs::canonicalize(s)
286                    .await
287                    .with_context(|| format!("failed to canonicalize absolute path [{s}]")),
288                Some(s) => tokio::fs::canonicalize(base_dir.as_ref().join(s))
289                    .await
290                    .with_context(|| format!("failed to canonicalize relative path [{s}]")),
291                None => bail!("invalid RegistryPullSource file path [{p}]"),
292            },
293            _ => bail!("registry pull source does not resolve to file path"),
294        }
295    }
296}
297
298impl TryFrom<RegistryPullSource> for RegistryMapping {
299    type Error = anyhow::Error;
300
301    fn try_from(value: RegistryPullSource) -> Result<Self> {
302        match value {
303            RegistryPullSource::Builtin | RegistryPullSource::LocalPath(_) => {
304                bail!("builtins and local files cannot be converted to registry mappings")
305            }
306            RegistryPullSource::RemoteHttp(_) => {
307                bail!("remote files HTTP files cannot be converted to registry mappings")
308            }
309            RegistryPullSource::RemoteGit(_) => {
310                bail!("remote git repositories files cannot be converted to registry mappings")
311            }
312            // For well known strings, we generally expect to receive a HTTP/S URL
313            RegistryPullSource::RemoteHttpWellKnown(url) => {
314                let url = Url::parse(&url).context("failed to parse url")?;
315                Registry::from_str(url.as_str())
316                    .map(RegistryMapping::Registry)
317                    .map_err(|e| anyhow!(e))
318            }
319            // For remote OCI images we expect to receive an 'oci://' prefixed String which we treat as a URI
320            //
321            // ex. `oci://ghcr.io/wasmcloud/interfaces` will turn into a registry with:
322            // - `ghcr.io` as the base
323            // - `wasmcloud/interfaces` as the namespace prefix
324            //
325            RegistryPullSource::RemoteOci(uri) => {
326                let url = Url::parse(&uri).context("failed to parse url")?;
327                if url.scheme() != "oci" {
328                    bail!("invalid scheme [{}], expected 'oci'", url.scheme());
329                }
330                let metadata = {
331                    let mut metadata = RegistryMetadata::default();
332                    metadata.preferred_protocol = Some("oci".into());
333                    let mut protocol_configs = serde_json::Map::new();
334                    let namespace_prefix = format!(
335                        "{}/",
336                        url.path().strip_prefix('/').unwrap_or_else(|| url.path())
337                    );
338                    protocol_configs.insert(
339                        "namespacePrefix".into(),
340                        serde_json::json!(namespace_prefix),
341                    );
342                    metadata.protocol_configs = HashMap::from([("oci".into(), protocol_configs)]);
343                    metadata
344                };
345                Ok(RegistryMapping::Custom(CustomConfig {
346                    registry: Registry::from_str(&format!(
347                        "{}{}",
348                        url.authority(),
349                        url.port().map(|p| format!(":{p}")).unwrap_or_default()
350                    ))
351                    .map_err(|e| anyhow!(e))?,
352                    metadata,
353                }))
354            }
355        }
356    }
357}
358
359/// Configuration common among all project types & languages.
360#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
361pub struct CommonConfig {
362    /// Name of the project.
363    pub name: String,
364    /// Semantic version of the project.
365    pub version: Version,
366    /// Monotonically increasing revision number
367    pub revision: i32,
368    /// Path to the project root to determine where build commands should be run.
369    pub project_dir: PathBuf,
370    /// Path to the directory where built artifacts should be written. Defaults to a `build` directory
371    /// in the project root.
372    pub build_dir: PathBuf,
373    /// Path to the directory where the WIT world and dependencies can be found. Defaults to a `wit`
374    /// directory in the project root.
375    pub wit_dir: PathBuf,
376    /// Expected name of the wasm module binary that will be generated
377    /// (if not present, name is expected to be used as a fallback)
378    pub wasm_bin_name: Option<String>,
379    /// Optional artifact OCI registry configuration. Primarily used for `wash push` & `wash pull` commands
380    pub registry: RegistryConfig,
381}
382
383impl CommonConfig {
384    /// Helper function to get the Wasm name, falling back to the project name if not specified
385    #[must_use]
386    pub fn wasm_bin_name(&self) -> String {
387        self.wasm_bin_name
388            .clone()
389            .unwrap_or_else(|| self.name.clone())
390    }
391}
392
393#[derive(Debug, Deserialize, Default, Clone, Eq, PartialEq)]
394pub enum WasmTarget {
395    #[default]
396    #[serde(alias = "wasm32-unknown-unknown")]
397    CoreModule,
398    #[serde(
399        alias = "wasm32-wasi",
400        alias = "wasm32-wasi-preview1",
401        alias = "wasm32-wasip1"
402    )]
403    WasiP1,
404    #[serde(
405        alias = "wasm32-wasip2",
406        alias = "wasm32-wasi-preview2",
407        alias = "wasm32-preview2"
408    )]
409    WasiP2,
410}
411
412impl From<&str> for WasmTarget {
413    fn from(value: &str) -> Self {
414        match value {
415            "wasm32-wasi-preview1" => WasmTarget::WasiP1,
416            "wasm32-wasip1" => WasmTarget::WasiP1,
417            "wasm32-wasi" => WasmTarget::WasiP1,
418            "wasm32-wasi-preview2" => WasmTarget::WasiP2,
419            "wasm32-wasip2" => WasmTarget::WasiP2,
420            "wasm32-unknown-unknown" => WasmTarget::CoreModule,
421            _ => {
422                warn!("Unknown wasm_target `{value}`, expected wasm32-wasip2 or wasm32-wasip1. Defaulting to wasm32-unknown-unknown");
423                WasmTarget::CoreModule
424            }
425        }
426    }
427}
428
429impl From<String> for WasmTarget {
430    fn from(value: String) -> Self {
431        value.as_str().into()
432    }
433}
434
435impl Display for WasmTarget {
436    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437        f.write_str(match &self {
438            WasmTarget::CoreModule => "wasm32-unknown-unknown",
439            WasmTarget::WasiP1 => "wasm32-wasip1",
440            WasmTarget::WasiP2 => "wasm32-wasip2",
441        })
442    }
443}
444
445/// Configuration related to Golang configuration
446#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
447pub struct GoConfig {
448    /// The path to the go binary. Optional, will default to `go` if not specified.
449    pub go_path: Option<PathBuf>,
450    /// Whether to disable the `go generate` step in the build process. Defaults to false.
451    #[serde(default)]
452    pub disable_go_generate: bool,
453}
454
455#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
456#[serde(rename_all = "snake_case")]
457pub enum TinyGoScheduler {
458    None,
459    Tasks,
460    Asyncify,
461}
462
463impl TinyGoScheduler {
464    pub fn as_str(&self) -> &'static str {
465        match self {
466            Self::None => "none",
467            Self::Tasks => "tasks",
468            Self::Asyncify => "asyncify",
469        }
470    }
471}
472
473#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
474#[serde(rename_all = "snake_case")]
475pub enum TinyGoGarbageCollector {
476    None,
477    Conservative,
478    Leaking,
479}
480
481impl TinyGoGarbageCollector {
482    pub fn as_str(&self) -> &'static str {
483        match self {
484            Self::None => "none",
485            Self::Conservative => "conservative",
486            Self::Leaking => "leaking",
487        }
488    }
489}
490
491#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
492pub struct TinyGoConfig {
493    /// The path to the tinygo binary. Optional, will default to `tinygo` if not specified.
494    pub tinygo_path: Option<PathBuf>,
495    /// Whether to disable the `go generate` step in the build process. Defaults to false.
496    #[serde(default)]
497    pub disable_go_generate: bool,
498    /// The scheduler to use for the TinyGo build.
499    ///
500    /// Override the default scheduler (asyncify). Valid values are: none, tasks, asyncify.
501    pub scheduler: Option<TinyGoScheduler>,
502    /// The garbage collector to use for the TinyGo build.
503    ///
504    /// Override the default garbage collector (conservative). Valid values are: none, conservative, leaking.
505    pub garbage_collector: Option<TinyGoGarbageCollector>,
506}
507
508impl TinyGoConfig {
509    #[must_use]
510    pub fn build_target(&self, wasm_target: &WasmTarget) -> &'static str {
511        match wasm_target {
512            WasmTarget::CoreModule => "wasm",
513            WasmTarget::WasiP1 => "wasi",
514            WasmTarget::WasiP2 => "wasip2",
515        }
516    }
517}
518
519/// Specification for how to wire up configuration
520#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
521#[serde(untagged)]
522pub enum DevConfigSpec {
523    /// Existing config with the given name
524    Named { name: String },
525    /// Explicitly specified configuration with all values presented
526    Values { values: BTreeMap<String, String> },
527}
528
529/// Specification for how to wire up secrets
530#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
531#[serde(untagged)]
532pub enum DevSecretSpec {
533    /// Existing secret with a name and source properties
534    Existing {
535        name: String,
536        source: SecretSourceProperty,
537    },
538    /// Explicitly specified secret values with all values presented
539    ///
540    /// NOTE: Secret names are required at all times, since secrets
541    /// *must* be named when interacting with a secret store
542    Values {
543        name: String,
544        values: BTreeMap<String, String>,
545    },
546}
547
548/// Target that specifies a single component in a given manifest path
549#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
550pub struct DevManifestComponentTarget {
551    /// Name of the component that should be targeted
552    pub component_name: Option<String>,
553
554    /// The ID of the component that should be targeted
555    pub component_id: Option<String>,
556
557    /// The image reference of the component that should be targeted
558    pub component_ref: Option<String>,
559
560    /// The manifest in which the target exists
561    pub path: PathBuf,
562}
563
564impl DevManifestComponentTarget {
565    pub fn matches(&self, component: &Component) -> bool {
566        let (component_id, component_ref) = match &component.properties {
567            Properties::Component { ref properties } => (&properties.id, &properties.image),
568            Properties::Capability { ref properties } => (&properties.id, &properties.image),
569        };
570
571        if self
572            .component_name
573            .as_ref()
574            .is_some_and(|v| v == &component.name)
575        {
576            return true;
577        }
578
579        if self
580            .component_id
581            .as_ref()
582            .is_some_and(|a| component_id.as_ref().is_some_and(|b| a == b))
583        {
584            return true;
585        }
586
587        if self
588            .component_ref
589            .as_ref()
590            .is_some_and(|v| component_ref.as_ref().is_some_and(|c| c == v))
591        {
592            return true;
593        }
594
595        false
596    }
597}
598
599/// Interface-based overrides used for a single component
600#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
601pub struct InterfaceComponentOverride {
602    /// Specification of the interface
603    ///
604    /// ex. `wasi:keyvalue@0.2.0`, `wasi:http/incoming-handler@0.2.0`
605    #[serde(alias = "interface")]
606    pub interface_spec: String,
607
608    /// Configuration that should be provided to the overridden component
609    pub config: Option<OneOrMore<DevConfigSpec>>,
610
611    /// Secrets that should be provided to the overridden component
612    pub secrets: Option<OneOrMore<DevSecretSpec>>,
613
614    /// Reference to the component
615    #[serde(alias = "uri")]
616    pub image_ref: Option<String>,
617
618    /// Link name that should be used to reference the component
619    ///
620    /// This is only required when there are *more than one* overrides that conflict (i.e. there is no "default")
621    pub link_name: Option<String>,
622}
623
624/// String that represents a specification of a WIT interface (normally used when specifying [`InterfaceComponentOverride`]s)
625#[derive(Debug, Clone, PartialEq, Eq)]
626pub struct WitInterfaceSpec {
627    /// WIT namespace
628    pub namespace: WitNamespace,
629    /// WIT package name
630    pub package: WitPackage,
631    /// WIT interfaces, if omitted will be used to match any interface
632    pub interfaces: Option<HashSet<WitInterface>>,
633    /// WIT interface function
634    pub function: Option<WitFunction>,
635    /// Version of WIT interface
636    pub version: Option<Version>,
637}
638
639impl WitInterfaceSpec {
640    /// Check whether this wit interface specification "contains" another one
641    ///
642    /// Containing another WIT interface spec means the current interface (if loosely specified)
643    /// is *more* general than the `other` one.
644    ///
645    /// This means that if the `other` spec is more general, this one will count as overlapping with it.
646    ///
647    /// ```
648    /// use std::str::FromStr;
649    /// use wash_lib::parser::WitInterfaceSpec;
650    /// assert!(WitInterfaceSpec::from_str("wasi:http").unwrap().includes(WitInterfaceSpec::from_str("wasi:http/incoming-handler").as_ref().unwrap()));
651    /// assert!(WitInterfaceSpec::from_str("wasi:http/incoming-handler").unwrap().includes(WitInterfaceSpec::from_str("wasi:http/incoming-handler.handle").as_ref().unwrap()));
652    /// ```
653    pub fn includes(&self, other: &Self) -> bool {
654        !self.is_disjoint(other)
655    }
656
657    pub fn is_disjoint(&self, other: &Self) -> bool {
658        if self.namespace != other.namespace {
659            return true;
660        }
661        if self.package != other.package {
662            return true;
663        }
664        // If interfaces don't match, this interface can't contain the other one
665        match (self.interfaces.as_ref(), other.interfaces.as_ref()) {
666            // If they both have no interface specified, then we do overlap
667            (None, None) |
668            // If the other has no interface, but this one does, this *does* overlap
669            (Some(_), None) |
670            // If this spec has no interface, but the other does, then we do overlap
671            (None, Some(_)) => {
672                return false;
673            }
674            // If both specify different interfaces, we don't overlap
675            (Some(iface), Some(other_iface)) if iface != other_iface => {
676                return true;
677            }
678            // The only option left is when the interfaces are the same
679            (Some(_), Some(_)) => {}
680        }
681
682        // At this point, we know that the interfaces must match
683        match (self.function.as_ref(), other.function.as_ref()) {
684            // If neither have functions, they cannot be disjoint
685            (None, None) |
686            // If only self has a function, then they are not disjoint
687            // (other contains self)
688            (Some(_), None) |
689            // If only the other has a function, then they are not disjoint
690            // (self contains other)
691            (None, Some(_)) => {
692                return false;
693            }
694            // If the functions differ, these are disjoint
695            (Some(f), Some(other_f)) if f != other_f => {
696                return true;
697            }
698            // The only option left is when the functions are the same
699            (Some(_), Some(_)) => {}
700        }
701
702        // Compare the versions
703        match (self.version.as_ref(), other.version.as_ref())  {
704            // If the neither have versions, they cannot be disjoint
705            (None, None) |
706            // If only self has a version, they cannot be disjoint
707            // (self contains other)
708            (Some(_), None) |
709            // If only the other has a version, they cannot be disjoint
710            // (other contains self)
711            (None, Some(_)) => {
712                false
713            }
714            // If the *either* version matches the other in semantic version terms, they cannot be disjoint
715            //
716            // Realistically this means that 0.2.0 and 0.2.1 are *not* disjoint, and while they could be,
717            // we assume that semantic versioning semantics should ensure that 0.2.0 and 0.2.1 are backwards compatible
718            // (though for <1.x versions, there is no such "real" guarantee)
719            //
720            (Some(v), Some(other_v)) if VersionReq::parse(&format!("^{v}")).is_ok_and(|req| req.matches(other_v)) => { false }
721            (Some(v), Some(other_v)) if VersionReq::parse(&format!("^{other_v}")).is_ok_and(|req| req.matches(v)) => {
722                false
723            }
724            // The only option left is that the versions are the same and their versions are incompatible/different
725            _ => true
726        }
727    }
728}
729
730impl std::str::FromStr for WitInterfaceSpec {
731    type Err = anyhow::Error;
732
733    fn from_str(s: &str) -> Result<Self> {
734        match parse_wit_package_name(s) {
735            Ok((namespace, packages, interfaces, function, version))
736                if packages.len() == 1
737                    && (interfaces.is_none()
738                        || interfaces.as_ref().is_some_and(|v| v.len() == 1)) =>
739            {
740                Ok(Self {
741                    namespace,
742                    package: packages
743                        .into_iter()
744                        .next()
745                        .context("unexpectedly missing package")?,
746                    interfaces: match interfaces {
747                        Some(v) if v.is_empty() => bail!("unexpectedly missing interface"),
748                        Some(v) => Some(v.into_iter().collect()),
749                        None => None,
750                    },
751                    function,
752                    version,
753                })
754            }
755            Ok((_, _, _, Some(_), _)) => {
756                bail!("function-level interface overrides are not yet supported")
757            }
758            Ok(_) => bail!("nested interfaces not yet supported"),
759            Err(e) => bail!("failed to parse WIT interface spec (\"{s}\"): {e}"),
760        }
761    }
762}
763
764impl<'de> Deserialize<'de> for WitInterfaceSpec {
765    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
766    where
767        D: serde::Deserializer<'de>,
768    {
769        #[derive(Deserialize)]
770        #[serde(untagged)]
771        enum Multi {
772            Stringified(String),
773            Explicit {
774                namespace: String,
775                package: String,
776                interface: Option<String>,
777                function: Option<String>,
778                version: Option<Version>,
779            },
780        }
781
782        match Multi::deserialize(deserializer)? {
783            Multi::Stringified(s) => Self::from_str(&s).map_err(|e| {
784                serde::de::Error::custom(format!(
785                    "failed to parse WIT interface specification: {e}"
786                ))
787            }),
788            Multi::Explicit {
789                namespace,
790                package,
791                interface,
792                function,
793                version,
794            } => Ok(Self {
795                namespace,
796                package,
797                interfaces: interface.map(|i| HashSet::from([i])),
798                function,
799                version,
800            }),
801        }
802    }
803}
804
805/// Facilitates *one* of a given `T` or more (primarily for serde use)
806#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
807#[serde(untagged)]
808pub enum OneOrMore<T> {
809    /// Only one of the given type
810    One(T),
811    /// More than one T
812    More(Vec<T>),
813}
814
815impl<T> OneOrMore<T> {
816    /// Convert this `OneOrMore<T>` into a `Vec<T>`
817    #[allow(unused)]
818    fn into_vec(self) -> Vec<T> {
819        match self {
820            OneOrMore::One(t) => vec![t],
821            OneOrMore::More(ts) => ts,
822        }
823    }
824
825    pub fn iter(&self) -> impl Iterator<Item = &T> {
826        OneOrMoreIterator {
827            inner: self,
828            idx: 0,
829        }
830    }
831}
832
833/// Iterator for [`OneOrMore`]
834pub struct OneOrMoreIterator<'a, T> {
835    inner: &'a OneOrMore<T>,
836    idx: usize,
837}
838
839impl<'a, T> Iterator for OneOrMoreIterator<'a, T> {
840    type Item = &'a T;
841
842    fn next(&mut self) -> Option<Self::Item> {
843        match (self.idx, self.inner) {
844            (0, OneOrMore::One(inner)) => {
845                if let Some(v) = self.idx.checked_add(1) {
846                    self.idx = v
847                }
848                Some(inner)
849            }
850            (_, OneOrMore::One(_)) => None,
851            (idx, OneOrMore::More(vs)) => {
852                if let Some(v) = self.idx.checked_add(1) {
853                    self.idx = v
854                }
855                vs.get(idx)
856            }
857        }
858    }
859}
860
861/// Configuration for imports that should be overridden
862#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
863pub struct InterfaceOverrides {
864    /// Imports that should be overridden
865    #[serde(default)]
866    pub imports: Vec<InterfaceComponentOverride>,
867
868    /// Exports that should be overridden
869    #[serde(default)]
870    pub exports: Vec<InterfaceComponentOverride>,
871}
872
873/// Configuration for development environments and/or DX related plugins
874#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
875pub struct DevConfig {
876    /// Top level override of the WADM application manifest(s) to use for development
877    ///
878    /// If this value is specified, tooling should strive to use the provided manifest where possible.
879    /// If unspecified, it is up to tools to generate a manifest from available information.
880    #[serde(default)]
881    pub manifests: Vec<DevManifestComponentTarget>,
882
883    /// Configuration values to be passed to the component
884    #[serde(default, alias = "configs")]
885    pub config: Vec<DevConfigSpec>,
886
887    /// Configuration values to be passed to the component
888    #[serde(default)]
889    pub secrets: Vec<DevSecretSpec>,
890
891    /// Interface-driven overrides
892    ///
893    /// Normally keyed by strings that represent an interface specification (e.g. `wasi:keyvalue/store@0.2.0-draft`)
894    #[serde(default)]
895    pub overrides: InterfaceOverrides,
896}
897
898/// Gets the wasmCloud project (component or provider) config.
899///
900/// The config can come from multiple sources: a specific toml file path, a folder with a `wasmcloud.toml` file inside it, or by default it looks for a `wasmcloud.toml` file in the current directory.
901///
902/// The user can also override the config file by setting environment variables with the prefix "WASMCLOUD_". This behavior can be disabled by setting `use_env` to false.
903/// For example, a user could set the variable `WASMCLOUD_RUST_CARGO_PATH` to override the default `cargo` path.
904///
905/// # Arguments
906/// * `opt_path` - The path to the config file. If None, it will look for a wasmcloud.toml file in the current directory.
907/// * `use_env` - Whether to use the environment variables or not. If false, it will not attempt to use environment variables. Defaults to true.
908pub async fn load_config(
909    opt_path: Option<PathBuf>,
910    use_env: Option<bool>,
911) -> Result<ProjectConfig> {
912    let project_dir = match opt_path.clone() {
913        Some(p) => p,
914        None => std::env::current_dir().context("failed to get current directory")?,
915    };
916
917    let path = if !project_dir.exists() {
918        bail!("path {} does not exist", project_dir.display());
919    } else {
920        fs::canonicalize(&project_dir).context("failed to canonicalize project path")?
921    };
922
923    let (wasmcloud_toml_dir, wasmcloud_toml_path) = if path.is_dir() {
924        let wasmcloud_path = path.join("wasmcloud.toml");
925        if !wasmcloud_path.is_file() {
926            bail!("failed to find wasmcloud.toml in [{}]", path.display());
927        }
928        (path, wasmcloud_path)
929    } else if path.is_file() {
930        (
931            path.parent()
932                .ok_or_else(|| anyhow!("Could not get parent path of wasmcloud.toml file"))?
933                .to_path_buf(),
934            path,
935        )
936    } else {
937        bail!(
938            "failed to find wasmcloud.toml: path [{}] is not a directory or file",
939            path.display()
940        );
941    };
942
943    let mut config = Config::builder().add_source(config::File::from(wasmcloud_toml_path.clone()));
944
945    if use_env.unwrap_or(true) {
946        config = config.add_source(config::Environment::with_prefix("WASMCLOUD"));
947    }
948
949    let json_value = config
950        .build()
951        .map_err(|e| {
952            if e.to_string().contains("is not of a registered file format") {
953                return anyhow!("invalid config file: {}", wasmcloud_toml_path.display());
954            }
955
956            anyhow!("{}", e)
957        })?
958        .try_deserialize::<serde_json::Value>()?;
959
960    let mut toml_project_config: WasmcloudDotToml = serde_json::from_value(json_value)?;
961    // NOTE(thomastaylor312): Because the package config fields have serde default, they get set,
962    // even if nothing was set in the toml file. So, if the config is equal to the default, we set
963    // things to None.
964    let current_config = toml_project_config
965        .package_config
966        .take()
967        .unwrap_or_default();
968    if current_config != PackageConfig::default() {
969        toml_project_config.package_config = Some(current_config);
970    }
971    if toml_project_config.package_config.is_none() {
972        // Attempt to load the package config from wkg.toml if it wasn't set in wasmcloud.toml
973        let wkg_toml_path = wasmcloud_toml_dir.join(wasm_pkg_core::config::CONFIG_FILE_NAME);
974        // If the file exists, we attempt to load it. We don't want to warn if it doesn't exist.
975        // If it does exist, we want to warn if it's invalid.
976        match tokio::fs::metadata(&wkg_toml_path).await {
977            Ok(meta) if meta.is_file() => {
978                match PackageConfig::load_from_path(wkg_toml_path).await {
979                    Ok(wkg_config) => {
980                        toml_project_config.package_config = Some(wkg_config);
981                    }
982                    Err(e) => {
983                        tracing::warn!(err = %e, "failed to load wkg.toml");
984                    }
985                }
986            }
987            Ok(_) => (),
988            Err(e) => {
989                if e.kind() != std::io::ErrorKind::NotFound {
990                    tracing::warn!(err = %e, "IO error when trying to fallback to wkg.toml");
991                }
992            }
993        };
994    }
995
996    toml_project_config
997        .convert(wasmcloud_toml_dir)
998        .map_err(|e: anyhow::Error| anyhow!("{} in {}", e, wasmcloud_toml_path.display()))
999}
1000
1001/// The wasmcloud.toml specification format as de-serialization friendly project configuration data
1002///
1003/// This structure is normally directly de-serialized from `wasmcloud.toml`,
1004/// and is used to build a more structured [`ProjectConfig`] object.
1005///
1006/// Below is an example of each option in the wasmcloud.toml file. A real example
1007/// only needs to include the fields that are relevant to the project.
1008///
1009/// ```rust
1010/// use wash_lib::parser::WasmcloudDotToml;
1011///
1012/// let component_toml = r#"
1013/// language = "rust"
1014/// type = "component"
1015/// name = "testcomponent"
1016/// version = "0.1.0"
1017/// "#;
1018/// let config: WasmcloudDotToml = toml::from_str(component_toml).expect("should deserialize");
1019/// eprintln!("{config:?}");
1020/// ```
1021#[derive(Deserialize, Debug)]
1022pub struct WasmcloudDotToml {
1023    /// The language of the project, e.g. rust, tinygo. This is used to determine which config to parse.
1024    pub language: String,
1025
1026    /// The type of project. This is a string that is used to determine which type of config to parse.
1027    /// The toml file name is just "type" but is named project_type here to avoid clashing with the type keyword in Rust.
1028    #[serde(rename = "type")]
1029    pub project_type: String,
1030
1031    /// Name of the project. Optional if building a Rust project, as it can be inferred from Cargo.toml.
1032    pub name: Option<String>,
1033
1034    /// Semantic version of the project. Optional if building a Rust project, as it can be inferred from Cargo.toml.
1035    pub version: Option<Version>,
1036
1037    /// Monotonically increasing revision number.
1038    #[serde(default)]
1039    pub revision: i32,
1040
1041    /// Path to the directory where the project is located. Defaults to the current directory.
1042    /// This path is where build commands will be run.
1043    pub path: Option<PathBuf>,
1044
1045    /// Path to the directory where the WIT world and dependencies can be found. Defaults to a `wit`
1046    /// directory in the project root.
1047    pub wit: Option<PathBuf>,
1048
1049    /// Path to the directory where the built artifacts should be written. Defaults to a `build`
1050    /// directory in the project root.
1051    pub build: Option<PathBuf>,
1052
1053    /// Configuration relevant to components
1054    #[serde(default)]
1055    pub component: ComponentConfig,
1056
1057    /// Configuration relevant to providers
1058    #[serde(default)]
1059    pub provider: ProviderConfig,
1060
1061    /// Rust configuration and options
1062    #[serde(default)]
1063    pub rust: RustConfig,
1064
1065    /// TinyGo related configuration and options
1066    #[serde(default)]
1067    pub tinygo: TinyGoConfig,
1068
1069    /// Golang related configuration and options
1070    #[serde(default)]
1071    pub go: GoConfig,
1072
1073    /// Configuration for development environments and/or DX related plugins
1074    #[serde(default)]
1075    pub dev: DevConfig,
1076
1077    /// Overrides for interface dependencies.
1078    ///
1079    /// This is often used to point to local wit files
1080    #[serde(flatten)]
1081    pub package_config: Option<PackageConfig>,
1082
1083    /// Configuration for image registry usage
1084    #[serde(default)]
1085    pub registry: RegistryConfig,
1086}
1087
1088impl WasmcloudDotToml {
1089    // Given a path to a valid cargo project, build an common_config enriched with Rust-specific information
1090    fn build_common_config_from_cargo_project(
1091        project_dir: PathBuf,
1092        build_dir: PathBuf,
1093        wit_dir: PathBuf,
1094        name: Option<String>,
1095        version: Option<Version>,
1096        revision: i32,
1097        registry: RegistryConfig,
1098    ) -> Result<CommonConfig> {
1099        let cargo_toml_path = project_dir.join("Cargo.toml");
1100        if !cargo_toml_path.is_file() {
1101            bail!(
1102                "missing/invalid Cargo.toml path [{}]",
1103                cargo_toml_path.display(),
1104            );
1105        }
1106
1107        // Build the manifest
1108        let mut cargo_toml = Manifest::from_path(cargo_toml_path)?;
1109
1110        // Populate Manifest with lib/bin information
1111        cargo_toml.complete_from_path(&project_dir)?;
1112
1113        let cargo_pkg = cargo_toml
1114            .package
1115            .ok_or_else(|| anyhow!("Missing package information in Cargo.toml"))?;
1116
1117        let version = match version {
1118            Some(version) => version,
1119            None => Version::parse(cargo_pkg.version.get()?.as_str())?,
1120        };
1121
1122        let name = name.unwrap_or(cargo_pkg.name);
1123
1124        // Determine the wasm module name from the [lib] section of Cargo.toml
1125        let wasm_bin_name = match cargo_toml.lib {
1126            Some(Product {
1127                name: Some(lib_name),
1128                ..
1129            }) => Some(lib_name),
1130            _ => None,
1131        };
1132
1133        Ok(CommonConfig {
1134            name,
1135            version,
1136            revision,
1137            wit_dir,
1138            build_dir,
1139            project_dir,
1140            wasm_bin_name,
1141            registry,
1142        })
1143    }
1144
1145    pub fn convert(self, wasmcloud_toml_dir: PathBuf) -> Result<ProjectConfig> {
1146        let project_type_config = match self.project_type.trim().to_lowercase().as_str() {
1147            "component" => TypeConfig::Component(self.component),
1148            "provider" => TypeConfig::Provider(self.provider),
1149            project_type => bail!("unknown project type: {project_type}"),
1150        };
1151
1152        let language_config = match self.language.trim().to_lowercase().as_str() {
1153            "rust" => LanguageConfig::Rust(self.rust),
1154            "go" => LanguageConfig::Go(self.go),
1155            "tinygo" => LanguageConfig::TinyGo(self.tinygo),
1156            other => LanguageConfig::Other(other.to_string()),
1157        };
1158
1159        // Use the provided `path` in the wasmcloud.toml file, or default to the current directory
1160        let project_path = self
1161            .path
1162            .map(|p| {
1163                // If the path in the wasmcloud.toml is absolute, use that directly.
1164                // Otherwise, join it with the project_path so that it's relative to the wasmcloud.toml
1165                if p.is_absolute() {
1166                    p
1167                } else {
1168                    wasmcloud_toml_dir.join(p)
1169                }
1170            })
1171            .unwrap_or_else(|| wasmcloud_toml_dir.clone());
1172        let project_path = project_path.canonicalize().with_context(|| {
1173            format!(
1174                "failed to canonicalize project path, ensure it exists: [{}]",
1175                project_path.display()
1176            )
1177        })?;
1178        let build_dir = self
1179            .build
1180            .map(|build_dir| {
1181                if build_dir.is_absolute() {
1182                    Ok(build_dir)
1183                } else {
1184                    // The build_dir is relative to the wasmcloud.toml file, so we need to join it with the wasmcloud_toml_dir
1185                    canonicalize_or_create(wasmcloud_toml_dir.join(build_dir.as_path()))
1186                }
1187            })
1188            .unwrap_or_else(|| Ok(project_path.join("build")))?;
1189        let wit_dir = self
1190            .wit
1191            .map(|wit_dir| {
1192                if wit_dir.is_absolute() {
1193                    Ok(wit_dir)
1194                } else {
1195                    // The wit_dir is relative to the wasmcloud.toml file, so we need to join it with the wasmcloud_toml_dir
1196                    wasmcloud_toml_dir
1197                        .join(wit_dir.as_path())
1198                        .canonicalize()
1199                        .with_context(|| {
1200                            format!(
1201                                "failed to canonicalize wit directory, ensure it exists: [{}]",
1202                                wit_dir.display()
1203                            )
1204                        })
1205                }
1206            })
1207            .unwrap_or_else(|| Ok(project_path.join("wit")))?;
1208
1209        let common_config = match language_config {
1210            LanguageConfig::Rust(_) => {
1211                match Self::build_common_config_from_cargo_project(
1212                    project_path.clone(),
1213                    build_dir.clone(),
1214                    wit_dir.clone(),
1215                    self.name.clone(),
1216                    self.version.clone(),
1217                    self.revision,
1218                    self.registry.clone(),
1219                ) {
1220                    // Successfully built with cargo information
1221                    Ok(cfg) => cfg,
1222
1223                    // Fallback to non-specific language usage if we at least have a name & version
1224                    Err(_) if self.name.is_some() && self.version.is_some() => CommonConfig {
1225                        name: self.name.unwrap(),
1226                        version: self.version.unwrap(),
1227                        revision: self.revision,
1228                        wasm_bin_name: None,
1229                        project_dir: project_path,
1230                        wit_dir,
1231                        build_dir,
1232                        registry: self.registry,
1233                    },
1234
1235                    Err(err) => {
1236                        bail!("No Cargo.toml file found in the current directory, and name/version unspecified: {err}")
1237                    }
1238                }
1239            }
1240
1241            LanguageConfig::Go(_) | LanguageConfig::TinyGo(_) | LanguageConfig::Other(_) => {
1242                CommonConfig {
1243                    name: self
1244                        .name
1245                        .ok_or_else(|| anyhow!("Missing name in wasmcloud.toml"))?,
1246                    version: self
1247                        .version
1248                        .ok_or_else(|| anyhow!("Missing version in wasmcloud.toml"))?,
1249                    revision: self.revision,
1250                    project_dir: project_path,
1251                    wasm_bin_name: None,
1252                    wit_dir,
1253                    build_dir,
1254                    registry: self.registry,
1255                }
1256            }
1257        };
1258
1259        let package_config = self
1260            .package_config
1261            .map(|mut package_config| {
1262                package_config.overrides = package_config.overrides.map(|overrides| {
1263                    // Each override can contain an absolute path or a relative (to the wasmcloud.toml) path to a local
1264                    // set of WIT dependencies. To support running the build process from anywhere, we need to canonicalize
1265                    // these paths.
1266                    overrides
1267                        .into_iter()
1268                        .map(|(k, mut v)| {
1269                            if let Some(path) = v.path.as_ref() {
1270                                trace!("canonicalizing override path: [{}]", path.display());
1271                                let path = if path.is_absolute() {
1272                                    path.clone()
1273                                } else {
1274                                    let override_path = wasmcloud_toml_dir.join(path);
1275                                    override_path.canonicalize().unwrap_or_else(|e| {
1276                                        warn!(
1277                                            ?e,
1278                                            "failed to canonicalize override path, falling back to: [{}]",
1279                                            override_path.display()
1280                                        );
1281                                        override_path
1282                                    })
1283                                };
1284                                v.path = Some(path);
1285                            }
1286                            (k, v)
1287                        })
1288                        .collect::<HashMap<String, Override>>()
1289                });
1290                package_config
1291            })
1292            .unwrap_or_default();
1293
1294        Ok(ProjectConfig {
1295            dev: self.dev,
1296            project_type: project_type_config,
1297            language: language_config,
1298            common: common_config,
1299            package_config,
1300            wasmcloud_toml_dir,
1301        })
1302    }
1303}
1304
1305/// Project configuration, normally specified in the root keys of a wasmcloud.toml file
1306#[derive(Deserialize, Debug, Clone)]
1307pub struct ProjectConfig {
1308    /// The language of the project, e.g. rust, tinygo. Contains specific configuration for that language.
1309    pub language: LanguageConfig,
1310    /// The type of project, e.g. component, provider, interface. Contains the specific configuration for that type.
1311    /// This is renamed to "type" but is named project_type here to avoid clashing with the type keyword in Rust.
1312    #[serde(rename = "type")]
1313    pub project_type: TypeConfig,
1314    /// Configuration common among all project types & languages.
1315    pub common: CommonConfig,
1316    /// Configuration for development environments and/or DX related plugins
1317    pub dev: DevConfig,
1318    /// Configuration for package tooling
1319    pub package_config: PackageConfig,
1320    /// The directory where the project wasmcloud.toml file is located
1321    #[serde(skip)]
1322    pub wasmcloud_toml_dir: PathBuf,
1323}
1324
1325impl ProjectConfig {
1326    pub fn resolve_registry_credentials(
1327        &self,
1328        registry: impl AsRef<str>,
1329    ) -> Result<RegistryCredential> {
1330        let credentials_file = &self.common.registry.push.credentials.clone();
1331
1332        let Some(credentials_file) = credentials_file else {
1333            bail!("No registry credentials path configured")
1334        };
1335
1336        if !credentials_file.exists() {
1337            bail!(
1338                "Provided registry credentials file ({}) does not exist",
1339                credentials_file.display()
1340            )
1341        }
1342
1343        let credentials = std::fs::read_to_string(credentials_file).with_context(|| {
1344            format!(
1345                "Failed to read registry credentials file {}",
1346                credentials_file.display()
1347            )
1348        })?;
1349
1350        let credentials = serde_json::from_str::<HashMap<String, RegistryCredential>>(&credentials)
1351            .with_context(|| {
1352                format!(
1353                    "Failed to parse registry credentials from file {}",
1354                    credentials_file.display()
1355                )
1356            })?;
1357
1358        let Some(credentials) = credentials.get(registry.as_ref()) else {
1359            bail!(
1360                "Unable to find credentials for {} in the configured registry credentials file",
1361                registry.as_ref()
1362            )
1363        };
1364
1365        Ok(credentials.clone())
1366    }
1367}
1368
1369/// Helper function to canonicalize a path or create it if it doesn't exist before
1370/// attempting to canonicalize it. This is a nice helper to ensure that we can attempt
1371/// to precreate directories like `build` before we start writing to them.
1372fn canonicalize_or_create(path: PathBuf) -> Result<PathBuf> {
1373    match path.canonicalize() {
1374        Ok(path) => Ok(path),
1375        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
1376            fs::create_dir_all(&path).with_context(|| {
1377                format!(
1378                    "failed to create directory [{}] before canonicalizing",
1379                    path.display()
1380                )
1381            })?;
1382            path.canonicalize().with_context(|| {
1383                format!(
1384                    "failed to canonicalize directory [{}] after creating it",
1385                    path.display()
1386                )
1387            })
1388        }
1389        Err(e) => {
1390            Err(e).with_context(|| format!("failed to canonicalize directory [{}]", path.display()))
1391        }
1392    }
1393}
1394
1395// TODO(joonas): Remove these once doctests are run as part of CI.
1396#[cfg(test)]
1397mod tests {
1398    use crate::parser::WitInterfaceSpec;
1399    use std::str::FromStr;
1400
1401    #[test]
1402    fn test_includes() {
1403        let wasi_http = WitInterfaceSpec::from_str("wasi:http")
1404            .expect("should parse 'wasi:http' into WitInterfaceSpec");
1405        let wasi_http_incoming_handler = WitInterfaceSpec::from_str("wasi:http/incoming-handler")
1406            .expect("should parse 'wasi:http/incoming-handler' into WitInterfaceSpec");
1407        let wasi_http_incoming_handler_handle =
1408            WitInterfaceSpec::from_str("wasi:http/incoming-handler.handle")
1409                .expect("should parse 'wasi:http/incoming-handler.handle' into WitInterfaceSpec");
1410        assert!(wasi_http.includes(&wasi_http_incoming_handler));
1411        assert!(wasi_http_incoming_handler.includes(&wasi_http_incoming_handler_handle));
1412    }
1413}