Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ license = "Apache-2.0 WITH LLVM-exception"
[workspace.dependencies]
brotli = "8.0.2"
clap = { version = "4.5.60", features = ["derive"] }
deterministic-wasi-ctx = "4.0.0"
wasmtime = "42"
wasmtime-wasi = "42"
wasmtime-wizer = "42"
Expand Down
20 changes: 20 additions & 0 deletions crates/cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ pub struct InitPluginCommandOpts {
#[arg(short, long = "out")]
/// Output path for the initialized plugin binary (default is stdout).
pub out: Option<PathBuf>,
#[arg(long)]
/// Produce deterministic output by using fixed clocks and constant
/// zero-filled RNG during pre-initialization. Security note: both
/// secure_random and insecure_random become non-secure.
pub deterministic: bool,
}

impl<T> ValueParserFactory for GroupOption<T>
Expand Down Expand Up @@ -151,6 +156,7 @@ pub struct CodegenOptionGroup {
pub wit: WitOptions,
pub source: Source,
pub plugin: Option<PathBuf>,
pub deterministic: bool,
}

impl Default for CodegenOptionGroup {
Expand All @@ -160,6 +166,7 @@ impl Default for CodegenOptionGroup {
wit: WitOptions::default(),
source: Source::Compressed,
plugin: None,
deterministic: false,
}
}
}
Expand All @@ -186,6 +193,11 @@ option_group! {
/// linked modules. JavaScript config options are also not supported when
/// using this parameter.
Plugin(PathBuf),
/// Produce deterministic output by using fixed clocks and constant
/// zero-filled RNG during pre-initialization. Ensures identical input
/// always produces identical output. Security note: both
/// secure_random and insecure_random become non-secure.
Deterministic(bool),
}
}

Expand All @@ -202,6 +214,7 @@ impl TryFrom<Vec<GroupOption<CodegenOption>>> for CodegenOptionGroup {
let mut wit_world_specified = false;
let mut source_specified = false;
let mut plugin_specified = false;
let mut deterministic_specified = false;

for option in value.iter().flat_map(|i| i.0.iter()) {
match option {
Expand Down Expand Up @@ -240,6 +253,13 @@ impl TryFrom<Vec<GroupOption<CodegenOption>>> for CodegenOptionGroup {
options.plugin = Some(path.clone());
plugin_specified = true;
}
CodegenOption::Deterministic(enabled) => {
if deterministic_specified {
bail!("deterministic can only be specified once");
}
options.deterministic = *enabled;
deterministic_specified = true;
}
}
}

Expand Down
8 changes: 6 additions & 2 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ async fn main() -> Result<()> {
generator.source_embedding(source_embedding);

set_producer_version(&mut generator);
generator.deterministic(codegen_opts.deterministic);

if codegen_opts.dynamic {
generator.linking(LinkingKind::Dynamic);
Expand All @@ -62,9 +63,12 @@ async fn main() -> Result<()> {
}
Command::InitPlugin(opts) => {
let plugin_bytes = fs::read(&opts.plugin)?;

let uninitialized_plugin = UninitializedPlugin::new(&plugin_bytes)?;
let initialized_plugin_bytes = uninitialized_plugin.initialize().await?;
let initialized_plugin_bytes = if opts.deterministic {
uninitialized_plugin.initialize_with_determinism().await?
} else {
uninitialized_plugin.initialize().await?
};

let mut out: Box<dyn Write> = match opts.out.as_ref() {
Some(path) => Box::new(File::create(path)?),
Expand Down
25 changes: 24 additions & 1 deletion crates/cli/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ impl<'a> UninitializedPlugin<'a> {
Ok(Self { bytes })
}

/// Initializes the plugin.
/// Initializes the plugin with deterministic clocks, RNG, and
/// single-threaded compilation so identical input produces identical output.
pub(crate) async fn initialize_with_determinism(&self) -> Result<Vec<u8>> {
javy_plugin_processing::initialize_plugin_with_determinism(self.bytes).await
}

/// Initializes the plugin with default (non-deterministic) configuration.
pub(crate) async fn initialize(&self) -> Result<Vec<u8>> {
javy_plugin_processing::initialize_plugin(self.bytes).await
}
Expand Down Expand Up @@ -107,4 +113,21 @@ mod tests {
fn encode_as_component(module: &[u8]) -> Result<Vec<u8>> {
ComponentEncoder::default().module(module)?.encode()
}

#[tokio::test]
async fn test_deterministic_init_plugin() -> Result<()> {
let plugin_bytes = super::PLUGIN_MODULE;

let plugin = UninitializedPlugin::new(plugin_bytes)?;
let first = plugin.initialize_with_determinism().await?;

let plugin = UninitializedPlugin::new(plugin_bytes)?;
let second = plugin.initialize_with_determinism().await?;

assert_eq!(
first, second,
"deterministic init-plugin must produce identical output"
);
Ok(())
}
}
77 changes: 77 additions & 0 deletions crates/cli/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,33 @@ fn test_same_module_outputs_different_random_result(builder: &mut Builder) -> Re
Ok(())
}

#[javy_cli_test]
fn test_deterministic_builds_produce_identical_wasm(builder: &mut Builder) -> Result<()> {
let scripts = [
"deterministic-complex.js",
"deterministic-math.js",
"deterministic-strings.js",
];

for script in &scripts {
let builds: Vec<Vec<u8>> = (0..5)
.map(|_| {
let mut b = builder.clone();
b.input(script).deterministic(true);
b.build().map(|r| r.wasm)
})
.collect::<Result<_>>()?;

for (i, wasm) in builds.iter().enumerate().skip(1) {
assert_eq!(
builds[0], *wasm,
"deterministic build of {script} #{i} differs from build #0",
);
}
}
Ok(())
}

#[javy_cli_test]
fn test_exported_default_arrow_fn(builder: &mut Builder) -> Result<()> {
let mut runner = builder
Expand Down Expand Up @@ -446,6 +473,56 @@ fn test_init_plugin() -> Result<()> {
Ok(())
}

#[test]
fn test_init_plugin_deterministic() -> Result<()> {
let uninitialized_plugin = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join(
std::path::Path::new("target")
.join("wasm32-wasip1")
.join("release")
.join("plugin.wasm"),
);

let init_deterministic = || -> Result<Vec<u8>> {
let output = Command::new(env!("CARGO_BIN_EXE_javy"))
.arg("init-plugin")
.arg("--deterministic")
.arg(uninitialized_plugin.to_str().unwrap())
.output()?;
if !output.status.success() {
bail!(
"init-plugin --deterministic failed: {}",
str::from_utf8(&output.stderr)?,
);
}
Ok(output.stdout)
};

let first = init_deterministic()?;
let second = init_deterministic()?;

assert_eq!(
first, second,
"init-plugin --deterministic must produce identical output across invocations"
);

// Verify the initialized plugin is functional.
let engine = Engine::default();
let mut linker = Linker::new(&engine);
wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |s| s)?;
let wasi = WasiCtxBuilder::new().build_p1();
let mut store = Store::new(&engine, wasi);
let module = Module::new(&engine, &first)?;
let instance = linker.instantiate(store.as_context_mut(), &module)?;
instance
.get_typed_func::<(i32, i32), i32>(store.as_context_mut(), "compile-src")?
.call(store.as_context_mut(), (0, 0))?;

Ok(())
}

fn run_with_u8s(r: &mut Runner, stdin: u8) -> (u8, String, u64) {
let (output, logs, fuel_consumed) = run(r, stdin.to_le_bytes().into());
assert_eq!(1, output.len());
Expand Down
Loading
Loading