From 1171a9427799166f7fa9d51510a6492d4edf25bd Mon Sep 17 00:00:00 2001 From: Ian McEachern Date: Fri, 24 Jul 2026 13:46:08 -0700 Subject: [PATCH] Make `wac plug` output deterministic `wac plug` collected its plugs into a `std::collections::HashMap` and then iterated it to register and instantiate them. Rust's `HashMap` has randomised iteration order, so the order in which plug components were emitted into the composed output varied between runs on byte-identical inputs. The outputs were always valid and semantically equivalent -- each plug's payload was embedded verbatim, and only its position in the outer component section (and the corresponding `instantiate` indices) moved -- but they were not byte-identical, which makes the command unusable in content-addressed or reproducible build pipelines. Observed with wac-cli 0.10.1 on linux/amd64 and darwin/arm64: - 2 plugs, 20 runs -> 2 distinct output digests - 4 plugs, 30 runs -> 16 distinct output digests (4! = 24 permutations) Command-line order did not help: running `--plug a --plug b` and `--plug b --plug a` produced the same set of digests in the same proportion, so the argument order was discarded before emission. Use `indexmap::IndexMap` (already a dependency) instead, which preserves insertion order. Plugs are now registered in command-line order, so the output is byte-identical across runs and the argument order is meaningful. With this change, on the same inputs: - 2 plugs, 20 runs -> 1 digest - 4 plugs, 30 runs -> 1 digest - reversing the two `--plug` arguments yields a different, stable digest Fixes #211 Signed-off-by: Ian McEachern --- src/commands/plug.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/plug.rs b/src/commands/plug.rs index 55fdf62..d08146c 100644 --- a/src/commands/plug.rs +++ b/src/commands/plug.rs @@ -152,8 +152,12 @@ impl PlugCommand { let socket = Package::from_bytes("socket", None, socket, graph.types_mut())?; let socket = graph.register_package(socket)?; - // Collect the plugs by their names - let mut plugs_by_name = std::collections::HashMap::<_, Vec<_>>::new(); + // Collect the plugs by their names. + // An insertion-ordered map is used so that plugs are registered and + // instantiated in command-line order, which makes the encoded output + // deterministic; iterating a `HashMap` here made the emitted component + // order vary between runs on identical inputs. + let mut plugs_by_name = indexmap::IndexMap::<_, Vec<_>>::new(); for plug in self.plugs.iter() { let name = match plug { #[cfg(feature = "registry")]