diff --git a/crates/cli/src/codegen/builder.rs b/crates/cli/src/codegen/builder.rs index 868d1be5..a70f500d 100644 --- a/crates/cli/src/codegen/builder.rs +++ b/crates/cli/src/codegen/builder.rs @@ -1,5 +1,6 @@ use crate::codegen::{CodeGen, CodeGenType, DynamicGenerator, StaticGenerator}; use anyhow::{bail, Result}; +use javy_config::Config; use std::path::PathBuf; /// Options for using WIT in the code generation process. @@ -79,18 +80,23 @@ impl CodeGenBuilder { } /// Build a [`CodeGenerator`]. - pub fn build(self) -> Result> + pub fn build(self, js_runtime_config: Config) -> Result> where T: CodeGen, { match T::classify() { - CodeGenType::Static => self.build_static(), - CodeGenType::Dynamic => self.build_dynamic(), + CodeGenType::Static => self.build_static(js_runtime_config), + CodeGenType::Dynamic => { + if js_runtime_config != Config::all() { + bail!("Cannot set JS runtime options when building a dynamic module") + } + self.build_dynamic() + } } } - fn build_static(self) -> Result> { - let mut static_gen = Box::new(StaticGenerator::new()); + fn build_static(self, js_runtime_config: Config) -> Result> { + let mut static_gen = Box::new(StaticGenerator::new(js_runtime_config)); static_gen.source_compression = self.source_compression; static_gen.wit_opts = self.wit_opts; diff --git a/crates/cli/src/codegen/static.rs b/crates/cli/src/codegen/static.rs index 24560c31..c43e51f8 100644 --- a/crates/cli/src/codegen/static.rs +++ b/crates/cli/src/codegen/static.rs @@ -26,17 +26,20 @@ pub(crate) struct StaticGenerator { function_exports: Exports, /// WIT options for code generation. pub wit_opts: WitOptions, + /// JS runtime options for code generation. + pub js_runtime_config: Config, } impl StaticGenerator { /// Creates a new [`StaticGenerator`]. - pub fn new() -> Self { + pub fn new(js_runtime_config: Config) -> Self { let engine = include_bytes!(concat!(env!("OUT_DIR"), "/engine.wasm")); Self { engine, source_compression: Default::default(), function_exports: Default::default(), wit_opts: Default::default(), + js_runtime_config, } } } @@ -71,9 +74,10 @@ impl CodeGen for StaticGenerator { .unwrap() .set_stdin(Box::new(ReadPipe::from(js.as_bytes()))); - WASI.get_mut() - .unwrap() - .push_env("JS_RUNTIME_CONFIG", &Config::all().bits().to_string())?; + WASI.get_mut().unwrap().push_env( + "JS_RUNTIME_CONFIG", + &self.js_runtime_config.bits().to_string(), + )?; }; let wasm = Wizer::new() diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index 045b4244..3c78ce50 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, bail}; use clap::{Parser, Subcommand}; +use javy_config::Config; use std::{path::PathBuf, str::FromStr}; use crate::codegen::WitOptions; @@ -91,6 +92,16 @@ pub struct BuildCommandOpts { )] /// Codegen options. pub codegen: Vec, + + #[arg( + short = 'J', + long = "js", + long_help = "Available JS runtime options: +-J redirect-stdout-to-stderr[=y|n] -- Redirects console.log to stderr. + " + )] + /// JS runtime options. + pub js_runtime: Vec, } #[derive(Debug, Parser)] @@ -110,6 +121,16 @@ pub struct CodegenOptionGroup { pub source_compression: bool, } +impl Default for CodegenOptionGroup { + fn default() -> Self { + Self { + dynamic: false, + wit: WitOptions::default(), + source_compression: true, + } + } +} + #[derive(Clone, Debug)] pub enum CodegenOption { /// Creates a smaller module that requires a dynamically linked QuickJS provider Wasm @@ -163,24 +184,89 @@ impl TryFrom> for CodegenOptionGroup { type Error = anyhow::Error; fn try_from(value: Vec) -> Result { - let mut dynamic = false; + let mut options = Self::default(); let mut wit = None; let mut wit_world = None; - let mut source_compression = true; for option in value { match option { - CodegenOption::Dynamic(enabled) => dynamic = enabled, + CodegenOption::Dynamic(enabled) => options.dynamic = enabled, CodegenOption::Wit(path) => wit = Some(path), CodegenOption::WitWorld(world) => wit_world = Some(world), - CodegenOption::SourceCompression(enabled) => source_compression = enabled, + CodegenOption::SourceCompression(enabled) => options.source_compression = enabled, } } - Ok(Self { - dynamic, - wit: WitOptions::from_tuple((wit, wit_world))?, - source_compression, - }) + options.wit = WitOptions::from_tuple((wit, wit_world))?; + Ok(options) + } +} + +#[derive(Debug, PartialEq)] +pub struct JsRuntimeOptionGroup { + /// Whether to redirect console.log to stderr. + pub redirect_stdout_to_stderr: bool, +} + +impl Default for JsRuntimeOptionGroup { + fn default() -> Self { + Self { + redirect_stdout_to_stderr: true, + } + } +} + +#[derive(Clone, Debug)] +pub enum JsRuntimeOption { + /// Whether to redirect console.log to stderr. + RedirectStdoutToStderr(bool), +} + +impl FromStr for JsRuntimeOption { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + let mut parts = s.splitn(2, '='); + let key = parts + .next() + .ok_or_else(|| anyhow!("Invalid JS runtime key"))?; + let value = parts.next(); + let option = match key { + "redirect-stdout-to-stderr" => Self::RedirectStdoutToStderr(match value { + None => true, + Some("y") => true, + Some("n") => false, + _ => bail!("Invalid value for redirect-stdout-to-stderr"), + }), + _ => bail!("Invalid JS runtime key"), + }; + Ok(option) + } +} + +impl From> for JsRuntimeOptionGroup { + fn from(value: Vec) -> Self { + let mut group = Self::default(); + + for option in value { + match option { + JsRuntimeOption::RedirectStdoutToStderr(enabled) => { + group.redirect_stdout_to_stderr = enabled; + } + } + } + + group + } +} + +impl From for Config { + fn from(value: JsRuntimeOptionGroup) -> Self { + let mut config = Self::all(); + config.set( + Config::REDIRECT_STDOUT_TO_STDERR, + value.redirect_stdout_to_stderr, + ); + config } } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 1c2f1149..fc3ae783 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -4,12 +4,13 @@ mod commands; mod js; mod wit; -use crate::codegen::{DynamicGenerator, StaticGenerator, WitOptions}; +use crate::codegen::WitOptions; use crate::commands::{Cli, Command, EmitProviderCommandOpts}; use anyhow::Result; use clap::Parser; -use codegen::CodeGenBuilder; -use commands::CodegenOptionGroup; +use codegen::{CodeGenBuilder, DynamicGenerator, StaticGenerator}; +use commands::{CodegenOptionGroup, JsRuntimeOptionGroup}; +use javy_config::Config; use js::JS; use std::fs; use std::fs::File; @@ -43,10 +44,11 @@ fn main() -> Result<()> { .source_compression(!opts.no_source_compression) .provider_version("2"); + let config = Config::all(); let mut gen = if opts.dynamic { - builder.build::()? + builder.build::(config)? } else { - builder.build::()? + builder.build::(config)? }; let wasm = gen.generate(&js)?; @@ -63,10 +65,11 @@ fn main() -> Result<()> { .source_compression(codegen.source_compression) .provider_version("2"); + let js_runtime_options: JsRuntimeOptionGroup = opts.js_runtime.clone().into(); let mut gen = if codegen.dynamic { - builder.build::()? + builder.build::(js_runtime_options.into())? } else { - builder.build::()? + builder.build::(js_runtime_options.into())? }; let wasm = gen.generate(&js)?; diff --git a/crates/cli/tests/dynamic_linking_test.rs b/crates/cli/tests/dynamic_linking_test.rs index 09880c80..c73e727f 100644 --- a/crates/cli/tests/dynamic_linking_test.rs +++ b/crates/cli/tests/dynamic_linking_test.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use javy_runner::Builder; +use javy_runner::{Builder, JavyCommand}; use std::path::{Path, PathBuf}; use std::str; @@ -152,6 +152,22 @@ fn test_producers_section_present() -> Result<()> { }) } +#[test] +fn test_using_runtime_flag_with_dynamic_triggers_error() -> Result<()> { + let build_result = Builder::default() + .root(root()) + .bin(BIN) + .input("console.js") + .preload("javy_quickjs_provider_v2".into(), provider_module_path()) + .command(JavyCommand::Build) + .redirect_stdout_to_stderr(false) + .build(); + assert!(build_result.is_err_and(|e| e + .to_string() + .contains("Error: Cannot set JS runtime options when building a dynamic module"))); + Ok(()) +} + #[test] // Temporarily ignore given that Javy.JSON is disabled by default. #[ignore] diff --git a/crates/cli/tests/integration_test.rs b/crates/cli/tests/integration_test.rs index 6a697963..dd1f7001 100644 --- a/crates/cli/tests/integration_test.rs +++ b/crates/cli/tests/integration_test.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use javy_runner::{Runner, RunnerError}; +use javy_runner::{Builder, Runner, RunnerError}; use std::path::PathBuf; use std::str; @@ -95,22 +95,59 @@ fn test_encoding() -> Result<()> { } #[test] -fn test_logging() -> Result<()> { - run_with_compile_and_build(|builder| { - let mut runner = builder - .root(sample_scripts()) - .bin(BIN) - .input("logging.js") - .build()?; +fn test_logging_with_compile() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("logging.js") + .command(javy_runner::JavyCommand::Compile) + .build()?; + + let (output, logs, fuel_consumed) = run(&mut runner, &[]); + assert!(output.is_empty()); + assert_eq!( + "hello world from console.log\nhello world from console.error\n", + logs.as_str(), + ); + assert_fuel_consumed_within_threshold(37309, fuel_consumed); + Ok(()) +} - let (_output, logs, fuel_consumed) = run(&mut runner, &[]); - assert_eq!( - "hello world from console.log\nhello world from console.error\n", - logs.as_str(), - ); - assert_fuel_consumed_within_threshold(34169, fuel_consumed); - Ok(()) - }) +#[test] +fn test_logging_without_redirect() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("logging.js") + .command(javy_runner::JavyCommand::Build) + .redirect_stdout_to_stderr(false) + .build()?; + + let (output, logs, fuel_consumed) = run(&mut runner, &[]); + assert_eq!(b"hello world from console.log\n".to_vec(), output); + assert_eq!("hello world from console.error\n", logs.as_str()); + assert_fuel_consumed_within_threshold(37485, fuel_consumed); + Ok(()) +} + +#[test] +fn test_logging_with_redirect() -> Result<()> { + let mut runner = Builder::default() + .root(sample_scripts()) + .bin(BIN) + .input("logging.js") + .command(javy_runner::JavyCommand::Build) + .redirect_stdout_to_stderr(true) + .build()?; + + let (output, logs, fuel_consumed) = run(&mut runner, &[]); + assert!(output.is_empty()); + assert_eq!( + "hello world from console.log\nhello world from console.error\n", + logs.as_str(), + ); + assert_fuel_consumed_within_threshold(37309, fuel_consumed); + Ok(()) } #[test] diff --git a/crates/runner/src/lib.rs b/crates/runner/src/lib.rs index b6ac3fcc..a54fe505 100644 --- a/crates/runner/src/lib.rs +++ b/crates/runner/src/lib.rs @@ -31,6 +31,8 @@ pub struct Builder { wit: Option, /// The name of the wit world. world: Option, + /// Whether console.log should write to stderr. + redirect_stdout_to_stderr: Option, built: bool, /// Preload the module at path, using the given instance name. preload: Option<(String, PathBuf)>, @@ -49,6 +51,7 @@ impl Default for Builder { built: false, preload: None, command: JavyCommand::Build, + redirect_stdout_to_stderr: None, } } } @@ -84,6 +87,11 @@ impl Builder { self } + pub fn redirect_stdout_to_stderr(&mut self, enabled: bool) -> &mut Self { + self.redirect_stdout_to_stderr = Some(enabled); + self + } + pub fn command(&mut self, command: JavyCommand) -> &mut Self { self.command = command; self @@ -106,6 +114,7 @@ impl Builder { wit, world, root, + redirect_stdout_to_stderr, built: _, preload, command, @@ -121,7 +130,15 @@ impl Builder { Runner::compile_static(bin_path, root, input, wit, world) } } - JavyCommand::Build => Runner::build(bin_path, root, input, wit, world, preload), + JavyCommand::Build => Runner::build( + bin_path, + root, + input, + wit, + world, + redirect_stdout_to_stderr, + preload, + ), } } } @@ -183,6 +200,7 @@ impl Runner { source: impl AsRef, wit: Option, world: Option, + redirect_stdout_to_stderr: Option, preload: Option<(String, PathBuf)>, ) -> Result { // This directory is unique and will automatically get deleted @@ -192,7 +210,14 @@ impl Runner { let js_file = root.join(source); let wit_file = wit.map(|p| root.join(p)); - let args = Self::build_args(&js_file, &wasm_file, &wit_file, &world, preload.is_some()); + let args = Self::build_args( + &js_file, + &wasm_file, + &wit_file, + &world, + preload.is_some(), + &redirect_stdout_to_stderr, + ); Self::exec_command(bin, root, args)?; @@ -380,6 +405,7 @@ impl Runner { wit: &Option, world: &Option, dynamic: bool, + redirect_stdout_to_stderr: &Option, ) -> Vec { let mut args = vec![ "build".to_string(), @@ -400,6 +426,14 @@ impl Runner { args.push("dynamic".to_string()); } + if let Some(redirect_stdout_to_stderr) = *redirect_stdout_to_stderr { + args.push("-J".to_string()); + args.push(format!( + "redirect-stdout-to-stderr={}", + if redirect_stdout_to_stderr { "y" } else { "n" } + )); + } + args } @@ -433,7 +467,11 @@ impl Runner { io::stderr().write_all(&output.stderr)?; if !output.status.success() { - bail!("terminated with status = {}", output.status); + bail!( + "terminated with status = {}, stderr = {}", + output.status, + std::str::from_utf8(&output.stderr).unwrap(), + ); } Ok(())