From 1710319fedffb4e4378f61511b9aea7404944985 Mon Sep 17 00:00:00 2001 From: itowlson Date: Thu, 27 Apr 2023 11:37:48 +1200 Subject: [PATCH] Check for Rust and wasm32-wasi on spin new and spin build Signed-off-by: itowlson --- Cargo.lock | 70 ++++++++ crates/build/Cargo.toml | 3 + crates/build/src/interaction.rs | 7 + crates/build/src/lib.rs | 58 +++++- crates/build/src/manifest.rs | 1 + crates/build/src/scripting.rs | 84 +++++++++ crates/templates/Cargo.toml | 2 + crates/templates/src/lib.rs | 1 + crates/templates/src/reader.rs | 6 +- crates/templates/src/run.rs | 2 + crates/templates/src/scripting.rs | 165 ++++++++++++++++++ crates/templates/src/store.rs | 5 + crates/templates/src/template.rs | 7 + .../content/.spinbuild/toolchain.rhai | 26 +++ templates/http-rust/content/spin.toml | 1 + .../metadata/scripts/after_instantiate.rhai | 24 +++ .../http-rust/metadata/spin-template.toml | 4 + 17 files changed, 460 insertions(+), 6 deletions(-) create mode 100644 crates/build/src/interaction.rs create mode 100644 crates/build/src/scripting.rs create mode 100644 crates/templates/src/scripting.rs create mode 100644 templates/http-rust/content/.spinbuild/toolchain.rhai create mode 100644 templates/http-rust/metadata/scripts/after_instantiate.rhai diff --git a/Cargo.lock b/Cargo.lock index 48d713a4cc..8a3c76bf95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", + "const-random", + "getrandom 0.2.8", "once_cell", "version_check", ] @@ -809,6 +811,28 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1080,6 +1104,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -4187,6 +4217,32 @@ dependencies = [ "winreg", ] +[[package]] +name = "rhai" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd29fa1f740be6dc91982013957e08c3c4232d7efcfe19e12da87d50bad47758" +dependencies = [ + "ahash 0.8.3", + "bitflags 1.3.2", + "instant", + "num-traits", + "rhai_codegen", + "smallvec", + "smartstring", +] + +[[package]] +name = "rhai_codegen" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db74e3fdd29d969a0ec1f8e79171a6f0f71d0429293656901db382d248c4c021" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ring" version = "0.16.20" @@ -4880,7 +4936,10 @@ name = "spin-build" version = "1.2.0-pre0" dependencies = [ "anyhow", + "dialoguer 0.10.3", "futures", + "is-terminal", + "rhai", "serde", "spin-loader", "subprocess", @@ -5202,6 +5261,7 @@ dependencies = [ "fs_extra", "heck 0.4.1", "indexmap", + "is-terminal", "itertools", "lazy_static", "liquid", @@ -5211,6 +5271,7 @@ dependencies = [ "path-absolutize", "pathdiff", "regex", + "rhai", "semver 1.0.16", "serde", "sha2 0.10.6", @@ -5566,6 +5627,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinytemplate" version = "1.2.1" diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml index 3473355a9e..f473df4676 100644 --- a/crates/build/Cargo.toml +++ b/crates/build/Cargo.toml @@ -6,7 +6,10 @@ edition = { workspace = true } [dependencies] anyhow = "1.0.57" +dialoguer = "0.10" futures = "0.3.21" +is-terminal = "0.4" +rhai = "1.13.0" serde = { version = "1.0", features = [ "derive" ] } spin-loader = { path = "../loader" } subprocess = "0.2.8" diff --git a/crates/build/src/interaction.rs b/crates/build/src/interaction.rs new file mode 100644 index 0000000000..577c875c0b --- /dev/null +++ b/crates/build/src/interaction.rs @@ -0,0 +1,7 @@ +// TODO: subset of spin_templates::interaction + +use dialoguer::Confirm; + +pub(crate) fn confirm(text: &str) -> std::io::Result { + Confirm::new().with_prompt(text).interact() +} diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index 6f97793b86..709431b0cd 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -2,7 +2,9 @@ //! A library for building Spin components. +mod interaction; mod manifest; +mod scripting; use anyhow::{anyhow, bail, Context, Result}; use spin_loader::local::parent_dir; @@ -24,18 +26,64 @@ pub async fn build(manifest_file: &Path) -> Result<()> { return Ok(()); } - app.components + let results = app + .components .into_iter() - .map(|c| build_component(c, &app_dir)) - .collect::, _>>()?; + .map(|c| (c.clone(), build_component(&c, &app_dir))) + .collect::>(); + + let mut fail_count = 0; + let mut checks = vec![]; + + for (c, br) in results { + if let Err(e) = br { + fail_count += 1; + if fail_count == 1 { + // Blank line before first summary line, others kept together + eprintln!(); + } + eprintln!("{e:#}"); + + let build_dir = ".spinbuild"; + if let Some(build) = &c.build { + if let Some(check) = &build.check { + let check = match &build.workdir { + None => app_dir.join(build_dir).join(check), + Some(wd) => app_dir.join(wd).join(build_dir).join(check), + }; + if check.exists() { + checks.push(check); + } + } + } + } + } + + if !checks.is_empty() { + let mut engine = rhai::Engine::new(); + scripting::register_functions(&mut engine); + for check in checks { + // Because we have to pipe output directly to the console, we can't pass it to the script. + // The script will have to assume the worst. + let check_result = engine.run_file(check.clone()); + if let Err(e) = check_result { + tracing::warn!("Check script error in {check:?}: {e:?}"); + } + } + eprintln!(); // Because one of the checks might have printed something and we want to keep it apart from the Rust termination message + } + + if fail_count > 0 { + bail!("Build failed for {fail_count} component(s)") + } println!("Successfully ran the build command for the Spin components."); Ok(()) } /// Run the build command of the component. -fn build_component(raw: RawComponentManifest, app_dir: &Path) -> Result<()> { - match raw.build { +fn build_component(raw: &RawComponentManifest, app_dir: &Path) -> Result<()> { + match raw.build.as_ref() { Some(b) => { println!( "Executing the build command for component {}: {}", diff --git a/crates/build/src/manifest.rs b/crates/build/src/manifest.rs index 14bcaa5b1d..ea796d4bd6 100644 --- a/crates/build/src/manifest.rs +++ b/crates/build/src/manifest.rs @@ -47,4 +47,5 @@ pub(crate) struct RawBuildConfig { pub command: String, pub workdir: Option, pub watch: Option>, + pub check: Option, } diff --git a/crates/build/src/scripting.rs b/crates/build/src/scripting.rs new file mode 100644 index 0000000000..de0be05e7d --- /dev/null +++ b/crates/build/src/scripting.rs @@ -0,0 +1,84 @@ +// TODO: subset of spin_templates::scripting + +use is_terminal::IsTerminal; + +pub(crate) fn register_functions(engine: &mut rhai::Engine) { + engine.register_fn("ask_yn", ask_yn); + engine.register_fn("exec", exec); + engine.register_fn("interactive", interactive); + engine + .register_type::() + .register_get("program_found", CommandOutput::program_found) + .register_get("exit_code", CommandOutput::exit_code) + .register_get("stdout", CommandOutput::stdout) + .register_get("stderr", CommandOutput::stderr); +} + +// Functions and types to be injected into the scripting engine + +fn exec( + command: rhai::ImmutableString, + args: rhai::Array, +) -> Result> { + let command = command.to_string(); + let args = args.iter().map(|item| item.to_string()).collect::>(); + let outputr = std::process::Command::new(command).args(args).output(); + + let output = match outputr { + Ok(output) => CommandOutput { + program_found: true, + exit_code: output.status.code().unwrap_or(-1), + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => CommandOutput { + program_found: false, + exit_code: -2, + stdout: "".to_owned(), + stderr: "".to_owned(), + }, + _ => return Err(Box::::from(e.to_string())), + }, + }; + + Ok(rhai::Dynamic::from(output)) +} + +fn ask_yn(text: rhai::ImmutableString) -> bool { + if !std::io::stderr().is_terminal() { + eprintln!("Answering 'no' to '{text}'"); + return false; + } + crate::interaction::confirm(text.as_ref()).unwrap_or(false) +} + +fn interactive() -> bool { + std::io::stderr().is_terminal() +} + +#[derive(Clone, Debug)] +struct CommandOutput { + program_found: bool, + exit_code: i32, + stdout: String, + stderr: String, +} + +impl CommandOutput { + fn program_found(&mut self) -> bool { + self.program_found + } + + fn exit_code(&mut self) -> i64 { + self.exit_code.into() + } + + fn stdout(&mut self) -> String { + self.stdout.clone() + } + + fn stderr(&mut self) -> String { + self.stderr.clone() + } +} diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml index d6c57bf288..1b514e586f 100644 --- a/crates/templates/Cargo.toml +++ b/crates/templates/Cargo.toml @@ -14,6 +14,7 @@ dirs = "3.0" fs_extra = "1.2" heck = "0.4" indexmap = { version = "1", features = ["serde"] } +is-terminal = "0.4" itertools = "0.10.3" lazy_static = "1.4.0" liquid = "0.23" @@ -23,6 +24,7 @@ liquid-lib = "0.23" path-absolutize = "3.0.13" pathdiff = "0.2.1" regex = "1.5.4" +rhai = "1.13.0" semver = "1.0" serde = { version = "1.0", features = [ "derive" ] } sha2 = "0.10.1" diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 1aa344acd0..904bec425e 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -15,6 +15,7 @@ mod manager; mod reader; mod renderer; mod run; +mod scripting; mod source; mod store; mod template; diff --git a/crates/templates/src/reader.rs b/crates/templates/src/reader.rs index b3859a26c6..3381876f6d 100644 --- a/crates/templates/src/reader.rs +++ b/crates/templates/src/reader.rs @@ -1,4 +1,7 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; use anyhow::Context; use indexmap::IndexMap; @@ -23,6 +26,7 @@ pub(crate) struct RawTemplateManifestV1 { pub add_component: Option, pub parameters: Option>, pub custom_filters: Option>, + pub scripts: Option>, } #[derive(Debug, Deserialize)] diff --git a/crates/templates/src/run.rs b/crates/templates/src/run.rs index 3c53eb8cb6..8b9e7978c3 100644 --- a/crates/templates/src/run.rs +++ b/crates/templates/src/run.rs @@ -65,6 +65,8 @@ impl Run { .and_then(|t| t.render()) .and_then_async(|o| async move { o.write().await }) .await + .and_then_async(|_| self.template.after_instantiate()) + .await .err() } diff --git a/crates/templates/src/scripting.rs b/crates/templates/src/scripting.rs new file mode 100644 index 0000000000..8c90dd2bd3 --- /dev/null +++ b/crates/templates/src/scripting.rs @@ -0,0 +1,165 @@ +use std::path::PathBuf; + +use anyhow::anyhow; +use indexmap::IndexMap; +use is_terminal::IsTerminal; + +use crate::store::TemplateLayout; + +#[derive(Debug)] +pub(crate) struct Scripts { + engine: rhai::Engine, + asts: IndexMap, +} + +impl Scripts { + pub(crate) async fn run(&self, script: Script) -> anyhow::Result<()> { + // TODO: consider being able to mark a script as advisory, i.e. not an error if it fails + match self.asts.get(&script) { + None => Ok(()), + Some(ast) => self + .engine + .run_ast(ast) + .map_err(|e| anyhow!("Script {script:?} failed with error {e:?}")), + } + } +} + +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) enum Script { + AfterInstantiate, +} + +impl std::convert::TryFrom<&str> for Script { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + match value { + "after_instantiate" => Ok(Self::AfterInstantiate), + _ => Err(anyhow!("Unknown script {value}")), + } + } +} + +pub(crate) fn load_scripts( + layout: &TemplateLayout, + raw: &Option>, +) -> anyhow::Result { + let mut engine = rhai::Engine::new(); + register_functions(&mut engine); + let asts = load_scripts_to_engine(layout, raw, &engine)?; + Ok(Scripts { engine, asts }) +} + +fn load_scripts_to_engine( + layout: &TemplateLayout, + raw: &Option>, + engine: &rhai::Engine, +) -> anyhow::Result> { + match raw { + None => Ok(IndexMap::new()), + Some(scripts) => scripts + .iter() + .map(|(id, rel)| load_script(layout, id, rel, engine)) + .collect(), + } +} + +fn load_script( + layout: &TemplateLayout, + id: &str, + rel: &PathBuf, + engine: &rhai::Engine, +) -> anyhow::Result<(Script, rhai::AST)> { + let script_path = layout.scripts_dir().join(rel); + if script_path.exists() { + let script = id.try_into()?; + let ast = engine + .compile_file(script_path) + .map_err(|e| anyhow!("Invalid script file {} for {id}: {e:?}", rel.display()))?; + Ok((script, ast)) + } else { + Err(anyhow!("Path {} not found for script {id}", rel.display())) + } +} + +fn register_functions(engine: &mut rhai::Engine) { + engine.register_fn("ask_yn", ask_yn); + engine.register_fn("exec", exec); + engine.register_fn("interactive", interactive); + engine + .register_type::() + .register_get("program_found", CommandOutput::program_found) + .register_get("exit_code", CommandOutput::exit_code) + .register_get("stdout", CommandOutput::stdout) + .register_get("stderr", CommandOutput::stderr); +} + +// Functions and types to be injected into the scripting engine + +fn exec( + command: rhai::ImmutableString, + args: rhai::Array, +) -> Result> { + let command = command.to_string(); + let args = args.iter().map(|item| item.to_string()).collect::>(); + let outputr = std::process::Command::new(command).args(args).output(); + + let output = match outputr { + Ok(output) => CommandOutput { + program_found: true, + exit_code: output.status.code().unwrap_or(-1), + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => CommandOutput { + program_found: false, + exit_code: -2, + stdout: "".to_owned(), + stderr: "".to_owned(), + }, + _ => return Err(Box::::from(e.to_string())), + }, + }; + + Ok(rhai::Dynamic::from(output)) +} + +fn ask_yn(text: rhai::ImmutableString) -> bool { + if !std::io::stderr().is_terminal() { + eprintln!("Answering 'no' to '{text}'"); + return false; + } + crate::interaction::confirm(text.as_ref()).unwrap_or(false) +} + +fn interactive() -> bool { + std::io::stderr().is_terminal() +} + +#[derive(Clone, Debug)] +struct CommandOutput { + program_found: bool, + exit_code: i32, + stdout: String, + stderr: String, +} + +impl CommandOutput { + fn program_found(&mut self) -> bool { + self.program_found + } + + fn exit_code(&mut self) -> i64 { + self.exit_code.into() + } + + fn stdout(&mut self) -> String { + self.stdout.clone() + } + + fn stderr(&mut self) -> String { + self.stderr.clone() + } +} diff --git a/crates/templates/src/store.rs b/crates/templates/src/store.rs index 2820663254..1803493d67 100644 --- a/crates/templates/src/store.rs +++ b/crates/templates/src/store.rs @@ -74,6 +74,7 @@ const METADATA_DIR_NAME: &str = "metadata"; const FILTERS_DIR_NAME: &str = "filters"; const CONTENT_DIR_NAME: &str = "content"; const SNIPPETS_DIR_NAME: &str = "snippets"; +const SCRIPTS_DIR_NAME: &str = "scripts"; const MANIFEST_FILE_NAME: &str = "spin-template.toml"; @@ -110,6 +111,10 @@ impl TemplateLayout { self.metadata_dir().join(SNIPPETS_DIR_NAME) } + pub fn scripts_dir(&self) -> PathBuf { + self.metadata_dir().join(SCRIPTS_DIR_NAME) + } + pub fn install_record_file(&self) -> PathBuf { self.template_dir.join(INSTALLATION_RECORD_FILE_NAME) } diff --git a/crates/templates/src/template.rs b/crates/templates/src/template.rs index 813e5b9016..75ea497529 100644 --- a/crates/templates/src/template.rs +++ b/crates/templates/src/template.rs @@ -12,6 +12,7 @@ use crate::{ custom_filters::CustomFilterParser, reader::{RawCustomFilter, RawParameter, RawTemplateManifest, RawTemplateVariant}, run::{Run, RunOptions}, + scripting::{Script, Scripts}, store::TemplateLayout, }; @@ -25,6 +26,7 @@ pub struct Template { trigger: TemplateTriggerCompatibility, variants: HashMap, parameters: Vec, + scripts: Scripts, custom_filters: Vec, snippets_dir: Option, content_dir: Option, // TODO: maybe always need a spin.toml file in there? @@ -146,6 +148,7 @@ impl Template { trigger: Self::parse_trigger_type(raw.trigger_type, layout), variants: Self::parse_template_variants(raw.new_application, raw.add_component), parameters: Self::parse_parameters(&raw.parameters)?, + scripts: crate::scripting::load_scripts(layout, &raw.scripts)?, custom_filters: Self::load_custom_filters(layout, &raw.custom_filters)?, snippets_dir, content_dir, @@ -384,6 +387,10 @@ impl Template { } } } + + pub(crate) async fn after_instantiate(&self) -> anyhow::Result<()> { + self.scripts.run(Script::AfterInstantiate).await + } } impl TemplateParameter { diff --git a/templates/http-rust/content/.spinbuild/toolchain.rhai b/templates/http-rust/content/.spinbuild/toolchain.rhai new file mode 100644 index 0000000000..e8c4af650a --- /dev/null +++ b/templates/http-rust/content/.spinbuild/toolchain.rhai @@ -0,0 +1,26 @@ +if !interactive() { + return; +} + +let lr = exec("rustup", ["target", "list", "--installed"]); + +if lr.program_found { + if lr.exit_code == 0 && lr.stdout.contains("wasm32-wasi") { + // all is well + } else { + print(); + print("This application requires the Rust `wasm32-wasi` target."); + if ask_yn("You do not have this target installed. Would you like Spin to install it for you?") { + let ir = exec("rustup", ["target", "install", "wasm32-wasi"]); + print(ir.stdout); + print(ir.stderr); + } else { + print("You can install it by running `rustup target install wasm32-wasi`."); + } + } +} else { + print(); + print("This application requires Rust with the `wasm32-wasi` target. Please:"); + print("1. Install Rust from https://rustup.rs/"); + print("2. Then run `rustup target install wasm32-wasi`."); +} diff --git a/templates/http-rust/content/spin.toml b/templates/http-rust/content/spin.toml index 58d065fd31..dc459b2233 100644 --- a/templates/http-rust/content/spin.toml +++ b/templates/http-rust/content/spin.toml @@ -14,3 +14,4 @@ route = "{{http-path}}" [component.build] command = "cargo build --target wasm32-wasi --release" watch = ["src/**/*.rs", "Cargo.toml"] +check = "toolchain.rhai" diff --git a/templates/http-rust/metadata/scripts/after_instantiate.rhai b/templates/http-rust/metadata/scripts/after_instantiate.rhai new file mode 100644 index 0000000000..4179aa3e2f --- /dev/null +++ b/templates/http-rust/metadata/scripts/after_instantiate.rhai @@ -0,0 +1,24 @@ +if !interactive() { + return; +} + +let lr = exec("rustup", ["target", "list", "--installed"]); + +if lr.program_found { + if lr.exit_code == 0 && lr.stdout.contains("wasm32-wasi") { + // all is well + } else { + print("This template requires the Rust `wasm32-wasi` target."); + if ask_yn("You do not have this target installed. Would you like Spin to install it for you?") { + let ir = exec("rustup", ["target", "install", "wasm32-wasi"]); + print(ir.stdout); + print(ir.stderr); + } else { + print("You can install it by running `rustup target install wasm32-wasi`."); + } + } +} else { + print("This template requires Rust with the `wasm32-wasi` target. Please:"); + print("1. Install Rust from https://rustup.rs/"); + print("2. Then run `rustup target install wasm32-wasi`."); +} diff --git a/templates/http-rust/metadata/spin-template.toml b/templates/http-rust/metadata/spin-template.toml index 398c9fb3d1..3bd8e5f5f5 100644 --- a/templates/http-rust/metadata/spin-template.toml +++ b/templates/http-rust/metadata/spin-template.toml @@ -3,6 +3,10 @@ id = "http-rust" description = "HTTP request handler using Rust" tags = ["http", "rust"] +# TODO: maybe the separate name is pointless +[scripts] +after_instantiate = "after_instantiate.rhai" + [add_component] skip_files = ["spin.toml"] skip_parameters = ["http-base"]