1use 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 #[serde(default = "default_key_directory")]
54 pub key_directory: PathBuf,
55 #[serde(default, deserialize_with = "wasm_target")]
57 pub wasm_target: WasmTarget,
58 pub wasip1_adapter_path: Option<PathBuf>,
60 pub wit_world: Option<String>,
62 pub tags: Option<HashSet<String>>,
64 pub build_artifact: Option<PathBuf>,
66 pub build_command: Option<String>,
70 pub destination: Option<PathBuf>,
72}
73
74fn 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 #[serde(default = "default_vendor")]
98 pub vendor: String,
99 pub wit_world: Option<String>,
101 #[serde(default = "default_os")]
103 pub os: String,
104 #[serde(default = "default_arch")]
106 pub arch: String,
107 pub rust_target: Option<String>,
109 pub bin_name: Option<String>,
111 #[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 pub cargo_path: Option<PathBuf>,
134 pub target_path: Option<PathBuf>,
136 #[serde(default)]
138 pub debug: bool,
139}
140
141#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
142pub struct RegistryConfig {
143 #[serde(flatten)]
146 pub push: RegistryPushConfig,
147
148 pub pull: Option<RegistryPullConfig>,
150}
151
152#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
153pub struct RegistryPushConfig {
154 pub url: Option<String>,
156
157 pub credentials: Option<PathBuf>,
159
160 #[serde(default)]
162 pub push_insecure: bool,
163}
164
165#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
167pub struct RegistryPullConfig {
168 pub sources: Vec<RegistryPullSourceOverride>,
170}
171
172#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
174pub struct RegistryPullSourceOverride {
175 pub target: String,
179
180 pub source: RegistryPullSource,
182}
183
184#[derive(Debug, Default, PartialEq, Eq, Clone)]
186pub enum RegistryPullSource {
187 #[default]
191 Builtin,
192
193 LocalPath(String),
201
202 RemoteHttpWellKnown(String),
204
205 RemoteOci(String),
212
213 RemoteHttp(String),
220
221 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 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 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#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
361pub struct CommonConfig {
362 pub name: String,
364 pub version: Version,
366 pub revision: i32,
368 pub project_dir: PathBuf,
370 pub build_dir: PathBuf,
373 pub wit_dir: PathBuf,
376 pub wasm_bin_name: Option<String>,
379 pub registry: RegistryConfig,
381}
382
383impl CommonConfig {
384 #[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#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
447pub struct GoConfig {
448 pub go_path: Option<PathBuf>,
450 #[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 pub tinygo_path: Option<PathBuf>,
495 #[serde(default)]
497 pub disable_go_generate: bool,
498 pub scheduler: Option<TinyGoScheduler>,
502 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#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
521#[serde(untagged)]
522pub enum DevConfigSpec {
523 Named { name: String },
525 Values { values: BTreeMap<String, String> },
527}
528
529#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
531#[serde(untagged)]
532pub enum DevSecretSpec {
533 Existing {
535 name: String,
536 source: SecretSourceProperty,
537 },
538 Values {
543 name: String,
544 values: BTreeMap<String, String>,
545 },
546}
547
548#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
550pub struct DevManifestComponentTarget {
551 pub component_name: Option<String>,
553
554 pub component_id: Option<String>,
556
557 pub component_ref: Option<String>,
559
560 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#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
601pub struct InterfaceComponentOverride {
602 #[serde(alias = "interface")]
606 pub interface_spec: String,
607
608 pub config: Option<OneOrMore<DevConfigSpec>>,
610
611 pub secrets: Option<OneOrMore<DevSecretSpec>>,
613
614 #[serde(alias = "uri")]
616 pub image_ref: Option<String>,
617
618 pub link_name: Option<String>,
622}
623
624#[derive(Debug, Clone, PartialEq, Eq)]
626pub struct WitInterfaceSpec {
627 pub namespace: WitNamespace,
629 pub package: WitPackage,
631 pub interfaces: Option<HashSet<WitInterface>>,
633 pub function: Option<WitFunction>,
635 pub version: Option<Version>,
637}
638
639impl WitInterfaceSpec {
640 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 match (self.interfaces.as_ref(), other.interfaces.as_ref()) {
666 (None, None) |
668 (Some(_), None) |
670 (None, Some(_)) => {
672 return false;
673 }
674 (Some(iface), Some(other_iface)) if iface != other_iface => {
676 return true;
677 }
678 (Some(_), Some(_)) => {}
680 }
681
682 match (self.function.as_ref(), other.function.as_ref()) {
684 (None, None) |
686 (Some(_), None) |
689 (None, Some(_)) => {
692 return false;
693 }
694 (Some(f), Some(other_f)) if f != other_f => {
696 return true;
697 }
698 (Some(_), Some(_)) => {}
700 }
701
702 match (self.version.as_ref(), other.version.as_ref()) {
704 (None, None) |
706 (Some(_), None) |
709 (None, Some(_)) => {
712 false
713 }
714 (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 _ => 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#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
807#[serde(untagged)]
808pub enum OneOrMore<T> {
809 One(T),
811 More(Vec<T>),
813}
814
815impl<T> OneOrMore<T> {
816 #[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
833pub 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#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
863pub struct InterfaceOverrides {
864 #[serde(default)]
866 pub imports: Vec<InterfaceComponentOverride>,
867
868 #[serde(default)]
870 pub exports: Vec<InterfaceComponentOverride>,
871}
872
873#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize)]
875pub struct DevConfig {
876 #[serde(default)]
881 pub manifests: Vec<DevManifestComponentTarget>,
882
883 #[serde(default, alias = "configs")]
885 pub config: Vec<DevConfigSpec>,
886
887 #[serde(default)]
889 pub secrets: Vec<DevSecretSpec>,
890
891 #[serde(default)]
895 pub overrides: InterfaceOverrides,
896}
897
898pub 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 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 let wkg_toml_path = wasmcloud_toml_dir.join(wasm_pkg_core::config::CONFIG_FILE_NAME);
974 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#[derive(Deserialize, Debug)]
1022pub struct WasmcloudDotToml {
1023 pub language: String,
1025
1026 #[serde(rename = "type")]
1029 pub project_type: String,
1030
1031 pub name: Option<String>,
1033
1034 pub version: Option<Version>,
1036
1037 #[serde(default)]
1039 pub revision: i32,
1040
1041 pub path: Option<PathBuf>,
1044
1045 pub wit: Option<PathBuf>,
1048
1049 pub build: Option<PathBuf>,
1052
1053 #[serde(default)]
1055 pub component: ComponentConfig,
1056
1057 #[serde(default)]
1059 pub provider: ProviderConfig,
1060
1061 #[serde(default)]
1063 pub rust: RustConfig,
1064
1065 #[serde(default)]
1067 pub tinygo: TinyGoConfig,
1068
1069 #[serde(default)]
1071 pub go: GoConfig,
1072
1073 #[serde(default)]
1075 pub dev: DevConfig,
1076
1077 #[serde(flatten)]
1081 pub package_config: Option<PackageConfig>,
1082
1083 #[serde(default)]
1085 pub registry: RegistryConfig,
1086}
1087
1088impl WasmcloudDotToml {
1089 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 let mut cargo_toml = Manifest::from_path(cargo_toml_path)?;
1109
1110 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 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 let project_path = self
1161 .path
1162 .map(|p| {
1163 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 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 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 Ok(cfg) => cfg,
1222
1223 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 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#[derive(Deserialize, Debug, Clone)]
1307pub struct ProjectConfig {
1308 pub language: LanguageConfig,
1310 #[serde(rename = "type")]
1313 pub project_type: TypeConfig,
1314 pub common: CommonConfig,
1316 pub dev: DevConfig,
1318 pub package_config: PackageConfig,
1320 #[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
1369fn 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#[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}