diff --git a/Cargo.lock b/Cargo.lock index 27c1f073d2..b23ccf79ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7600,6 +7600,7 @@ dependencies = [ "dunce", "futures", "glob", + "indexmap 1.9.3", "itertools 0.10.5", "lazy_static 1.4.0", "mime_guess", diff --git a/crates/loader/Cargo.toml b/crates/loader/Cargo.toml index b04b148766..afdc38b447 100644 --- a/crates/loader/Cargo.toml +++ b/crates/loader/Cargo.toml @@ -12,6 +12,7 @@ dirs = "4.0" dunce = "1.0" futures = "0.3.17" glob = "0.3.0" +indexmap = { version = "1" } itertools = "0.10.3" lazy_static = "1.4.0" mime_guess = { version = "2.0" } diff --git a/crates/loader/src/lib.rs b/crates/loader/src/lib.rs index 90e4631a9d..ea64bac40d 100644 --- a/crates/loader/src/lib.rs +++ b/crates/loader/src/lib.rs @@ -40,6 +40,14 @@ pub async fn from_file( loader.load_file(path).await } +/// Load a Spin locked app from a standalone Wasm file. +pub async fn from_wasm_file(wasm_path: impl AsRef) -> Result { + let app_root = std::env::current_dir()?; + let manifest = single_file_manifest(wasm_path)?; + let loader = LocalLoader::new(&app_root, FilesMountStrategy::Direct, None).await?; + loader.load_manifest(manifest).await +} + /// The strategy to use for mounting WASI files into a guest. #[derive(Debug)] pub enum FilesMountStrategy { @@ -50,3 +58,36 @@ pub enum FilesMountStrategy { /// patterns, and `exclude_files` are not supported. Direct, } + +fn single_file_manifest( + wasm_path: impl AsRef, +) -> anyhow::Result { + use serde::Deserialize; + + let wasm_path_str = wasm_path + .as_ref() + .to_str() + .context("Failed to stringise Wasm file path")? + .to_owned(); + let app_name = wasm_path + .as_ref() + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("wasm-file") + .to_owned(); + + let manifest = toml::toml!( + spin_manifest_version = 2 + + [application] + name = app_name + + [[trigger.http]] + route = "/..." + component = { source = wasm_path_str } + ); + + let manifest = spin_manifest::schema::v2::AppManifest::deserialize(manifest)?; + + Ok(manifest) +} diff --git a/crates/loader/src/local.rs b/crates/loader/src/local.rs index 2c18a32a23..3b2303e820 100644 --- a/crates/loader/src/local.rs +++ b/crates/loader/src/local.rs @@ -67,7 +67,7 @@ impl LocalLoader { } // Load the given manifest into a LockedApp, ready for execution. - async fn load_manifest(&self, mut manifest: AppManifest) -> Result { + pub(crate) async fn load_manifest(&self, mut manifest: AppManifest) -> Result { spin_manifest::normalize::normalize_manifest(&mut manifest); let AppManifest { diff --git a/crates/manifest/src/schema/v2.rs b/crates/manifest/src/schema/v2.rs index 9fb5fd556a..180b200b97 100644 --- a/crates/manifest/src/schema/v2.rs +++ b/crates/manifest/src/schema/v2.rs @@ -118,10 +118,10 @@ pub struct Component { pub exclude_files: Vec, /// `allowed_http_hosts = ["example.com"]` #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub(crate) allowed_http_hosts: Vec, + pub allowed_http_hosts: Vec, /// `allowed_outbound_hosts = ["redis://myredishost.com:6379"]` #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub(crate) allowed_outbound_hosts: Vec, + pub allowed_outbound_hosts: Vec, /// `key_value_stores = ["default", "my-store"]` #[serde( default, diff --git a/examples/spin-timer/Cargo.lock b/examples/spin-timer/Cargo.lock index a87d1651b8..566a988379 100644 --- a/examples/spin-timer/Cargo.lock +++ b/examples/spin-timer/Cargo.lock @@ -5847,6 +5847,7 @@ dependencies = [ "dunce", "futures", "glob", + "indexmap 1.9.3", "itertools 0.10.5", "lazy_static 1.4.0", "mime_guess", diff --git a/src/commands/up.rs b/src/commands/up.rs index dabb5b035d..a22e523d29 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -435,6 +435,9 @@ impl UpCommand { .await?; ResolvedAppSource::OciRegistry { locked_app } } + AppSource::BareWasm(path) => ResolvedAppSource::BareWasm { + wasm_path: path.clone(), + }, AppSource::Unresolvable(err) => bail!("{err}"), AppSource::None => bail!("Internal error - should have shown help"), }) @@ -463,6 +466,11 @@ impl UpCommand { }) } ResolvedAppSource::OciRegistry { locked_app } => Ok(locked_app), + ResolvedAppSource::BareWasm { wasm_path } => spin_loader::from_wasm_file(&wasm_path) + .await + .with_context(|| { + format!("Failed to load component from {}", quoted_path(&wasm_path)) + }), } } diff --git a/src/commands/up/app_source.rs b/src/commands/up/app_source.rs index 88431ce76f..4c7ab656f6 100644 --- a/src/commands/up/app_source.rs +++ b/src/commands/up/app_source.rs @@ -13,6 +13,7 @@ use spin_manifest::schema::v2::AppManifest; pub enum AppSource { File(PathBuf), OciRegistry(String), + BareWasm(PathBuf), Unresolvable(String), None, } @@ -31,7 +32,13 @@ impl AppSource { pub fn infer_file_source(path: impl Into) -> Self { match spin_common::paths::resolve_manifest_file_path(path.into()) { - Ok(file) => Self::File(file), + Ok(file) => { + if is_wasm_file(&file) { + Self::BareWasm(file) + } else { + Self::File(file) + } + } Err(e) => Self::Unresolvable(e.to_string()), } } @@ -58,11 +65,17 @@ impl AppSource { } } +fn is_wasm_file(path: &Path) -> bool { + let extn = path.extension().and_then(std::ffi::OsStr::to_str); + extn.is_some_and(|e| e == "wasm" || e == "wat") +} + impl std::fmt::Display for AppSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::File(path) => write!(f, "local app {}", quoted_path(path)), Self::OciRegistry(reference) => write!(f, "remote app {reference:?}"), + Self::BareWasm(path) => write!(f, "Wasm file {}", quoted_path(path)), Self::Unresolvable(s) => write!(f, "unknown app source: {s:?}"), Self::None => write!(f, ""), } @@ -77,6 +90,9 @@ pub enum ResolvedAppSource { manifest_path: PathBuf, manifest: AppManifest, }, + BareWasm { + wasm_path: PathBuf, + }, OciRegistry { locked_app: LockedApp, }, @@ -85,17 +101,20 @@ pub enum ResolvedAppSource { impl ResolvedAppSource { pub fn trigger_types(&self) -> anyhow::Result> { let types = match self { - ResolvedAppSource::File { manifest, .. } => { - manifest.triggers.keys().collect::>() - } + ResolvedAppSource::File { manifest, .. } => manifest + .triggers + .keys() + .map(|s| s.as_str()) + .collect::>(), ResolvedAppSource::OciRegistry { locked_app } => locked_app .triggers .iter() - .map(|t| &t.trigger_type) + .map(|t| t.trigger_type.as_str()) .collect::>(), + ResolvedAppSource::BareWasm { .. } => ["http"].into_iter().collect(), }; ensure!(!types.is_empty(), "no triggers in app"); - Ok(types.into_iter().map(|t| t.as_str()).collect()) + Ok(types.into_iter().collect()) } }