Skip to content

Commit c14a222

Browse files
authored
Merge pull request #2479 from itowlson/manifestless
Run a Wasm file as an application without a manifest
2 parents 9457de5 + 3d562c4 commit c14a222

File tree

8 files changed

+80
-9
lines changed

8 files changed

+80
-9
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/loader/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dirs = "4.0"
1212
dunce = "1.0"
1313
futures = "0.3.17"
1414
glob = "0.3.0"
15+
indexmap = { version = "1" }
1516
itertools = "0.10.3"
1617
lazy_static = "1.4.0"
1718
mime_guess = { version = "2.0" }

crates/loader/src/lib.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ pub async fn from_file(
4040
loader.load_file(path).await
4141
}
4242

43+
/// Load a Spin locked app from a standalone Wasm file.
44+
pub async fn from_wasm_file(wasm_path: impl AsRef<Path>) -> Result<LockedApp> {
45+
let app_root = std::env::current_dir()?;
46+
let manifest = single_file_manifest(wasm_path)?;
47+
let loader = LocalLoader::new(&app_root, FilesMountStrategy::Direct, None).await?;
48+
loader.load_manifest(manifest).await
49+
}
50+
4351
/// The strategy to use for mounting WASI files into a guest.
4452
#[derive(Debug)]
4553
pub enum FilesMountStrategy {
@@ -50,3 +58,36 @@ pub enum FilesMountStrategy {
5058
/// patterns, and `exclude_files` are not supported.
5159
Direct,
5260
}
61+
62+
fn single_file_manifest(
63+
wasm_path: impl AsRef<Path>,
64+
) -> anyhow::Result<spin_manifest::schema::v2::AppManifest> {
65+
use serde::Deserialize;
66+
67+
let wasm_path_str = wasm_path
68+
.as_ref()
69+
.to_str()
70+
.context("Failed to stringise Wasm file path")?
71+
.to_owned();
72+
let app_name = wasm_path
73+
.as_ref()
74+
.file_stem()
75+
.and_then(|s| s.to_str())
76+
.unwrap_or("wasm-file")
77+
.to_owned();
78+
79+
let manifest = toml::toml!(
80+
spin_manifest_version = 2
81+
82+
[application]
83+
name = app_name
84+
85+
[[trigger.http]]
86+
route = "/..."
87+
component = { source = wasm_path_str }
88+
);
89+
90+
let manifest = spin_manifest::schema::v2::AppManifest::deserialize(manifest)?;
91+
92+
Ok(manifest)
93+
}

crates/loader/src/local.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ impl LocalLoader {
6767
}
6868

6969
// Load the given manifest into a LockedApp, ready for execution.
70-
async fn load_manifest(&self, mut manifest: AppManifest) -> Result<LockedApp> {
70+
pub(crate) async fn load_manifest(&self, mut manifest: AppManifest) -> Result<LockedApp> {
7171
spin_manifest::normalize::normalize_manifest(&mut manifest);
7272

7373
let AppManifest {

crates/manifest/src/schema/v2.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ pub struct Component {
118118
pub exclude_files: Vec<String>,
119119
/// `allowed_http_hosts = ["example.com"]`
120120
#[serde(default, skip_serializing_if = "Vec::is_empty")]
121-
pub(crate) allowed_http_hosts: Vec<String>,
121+
pub allowed_http_hosts: Vec<String>,
122122
/// `allowed_outbound_hosts = ["redis://myredishost.com:6379"]`
123123
#[serde(default, skip_serializing_if = "Vec::is_empty")]
124-
pub(crate) allowed_outbound_hosts: Vec<String>,
124+
pub allowed_outbound_hosts: Vec<String>,
125125
/// `key_value_stores = ["default", "my-store"]`
126126
#[serde(
127127
default,

examples/spin-timer/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/up.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,9 @@ impl UpCommand {
435435
.await?;
436436
ResolvedAppSource::OciRegistry { locked_app }
437437
}
438+
AppSource::BareWasm(path) => ResolvedAppSource::BareWasm {
439+
wasm_path: path.clone(),
440+
},
438441
AppSource::Unresolvable(err) => bail!("{err}"),
439442
AppSource::None => bail!("Internal error - should have shown help"),
440443
})
@@ -463,6 +466,11 @@ impl UpCommand {
463466
})
464467
}
465468
ResolvedAppSource::OciRegistry { locked_app } => Ok(locked_app),
469+
ResolvedAppSource::BareWasm { wasm_path } => spin_loader::from_wasm_file(&wasm_path)
470+
.await
471+
.with_context(|| {
472+
format!("Failed to load component from {}", quoted_path(&wasm_path))
473+
}),
466474
}
467475
}
468476

src/commands/up/app_source.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use spin_manifest::schema::v2::AppManifest;
1313
pub enum AppSource {
1414
File(PathBuf),
1515
OciRegistry(String),
16+
BareWasm(PathBuf),
1617
Unresolvable(String),
1718
None,
1819
}
@@ -31,7 +32,13 @@ impl AppSource {
3132

3233
pub fn infer_file_source(path: impl Into<PathBuf>) -> Self {
3334
match spin_common::paths::resolve_manifest_file_path(path.into()) {
34-
Ok(file) => Self::File(file),
35+
Ok(file) => {
36+
if is_wasm_file(&file) {
37+
Self::BareWasm(file)
38+
} else {
39+
Self::File(file)
40+
}
41+
}
3542
Err(e) => Self::Unresolvable(e.to_string()),
3643
}
3744
}
@@ -58,11 +65,17 @@ impl AppSource {
5865
}
5966
}
6067

68+
fn is_wasm_file(path: &Path) -> bool {
69+
let extn = path.extension().and_then(std::ffi::OsStr::to_str);
70+
extn.is_some_and(|e| e == "wasm" || e == "wat")
71+
}
72+
6173
impl std::fmt::Display for AppSource {
6274
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6375
match self {
6476
Self::File(path) => write!(f, "local app {}", quoted_path(path)),
6577
Self::OciRegistry(reference) => write!(f, "remote app {reference:?}"),
78+
Self::BareWasm(path) => write!(f, "Wasm file {}", quoted_path(path)),
6679
Self::Unresolvable(s) => write!(f, "unknown app source: {s:?}"),
6780
Self::None => write!(f, "<no source>"),
6881
}
@@ -77,6 +90,9 @@ pub enum ResolvedAppSource {
7790
manifest_path: PathBuf,
7891
manifest: AppManifest,
7992
},
93+
BareWasm {
94+
wasm_path: PathBuf,
95+
},
8096
OciRegistry {
8197
locked_app: LockedApp,
8298
},
@@ -85,17 +101,20 @@ pub enum ResolvedAppSource {
85101
impl ResolvedAppSource {
86102
pub fn trigger_types(&self) -> anyhow::Result<Vec<&str>> {
87103
let types = match self {
88-
ResolvedAppSource::File { manifest, .. } => {
89-
manifest.triggers.keys().collect::<HashSet<_>>()
90-
}
104+
ResolvedAppSource::File { manifest, .. } => manifest
105+
.triggers
106+
.keys()
107+
.map(|s| s.as_str())
108+
.collect::<HashSet<_>>(),
91109
ResolvedAppSource::OciRegistry { locked_app } => locked_app
92110
.triggers
93111
.iter()
94-
.map(|t| &t.trigger_type)
112+
.map(|t| t.trigger_type.as_str())
95113
.collect::<HashSet<_>>(),
114+
ResolvedAppSource::BareWasm { .. } => ["http"].into_iter().collect(),
96115
};
97116

98117
ensure!(!types.is_empty(), "no triggers in app");
99-
Ok(types.into_iter().map(|t| t.as_str()).collect())
118+
Ok(types.into_iter().collect())
100119
}
101120
}

0 commit comments

Comments
 (0)