Skip to content

Commit c4c036b

Browse files
kraemctron
authored andcommitted
fix: hash each wasm-bindgen artifact by its own content
the JS loader was named from the wasm digest but its SRI integrity comes from the JS bytes. wasm-bindgen JS is non-deterministic for identical wasm, so a rebuild reused the name with a different integrity and broke SRI. hash js/wasm/loader each from their own bytes so name and integrity stay in sync. fixes #1028.
1 parent 5ccc168 commit c4c036b

1 file changed

Lines changed: 41 additions & 30 deletions

File tree

src/pipelines/rust/mod.rs

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use std::{
3636
str::FromStr,
3737
sync::Arc,
3838
};
39-
use tokio::{fs, io::AsyncWriteExt, process::Command, sync::mpsc, task::JoinHandle};
39+
use tokio::{fs, process::Command, sync::mpsc, task::JoinHandle};
4040
use tracing::log;
4141
use wasm_bindgen::{WasmBindgenFeatures, WasmBindgenTarget, find_wasm_bindgen_version};
4242
use wasm_opt::WasmOptLevel;
@@ -607,29 +607,33 @@ impl RustApp {
607607

608608
// Copy the generated WASM & JS loader to the dist dir.
609609
tracing::debug!("copying generated wasm-bindgen artifacts");
610-
let hashed_name = self.hashed_wasm_base(wasm_path).await?;
611-
let hashed_wasm_name =
612-
apply_data_target_path(format!("{hashed_name}_bg.wasm"), &self.target_path);
613610

614611
let js_name = format!("{}.js", self.name);
615-
let hashed_js_name = apply_data_target_path(format!("{hashed_name}.js"), &self.target_path);
612+
let js_loader_path = bindgen_out.join(&js_name);
613+
let wasm_name = format!("{}_bg.wasm", self.name);
614+
let wasm_path = bindgen_out.join(&wasm_name);
615+
616+
// Hash each artifact by its own wasm-bindgen output, so a file's hashed name changes
617+
// exactly when that file's bytes change and the name stays in lockstep with the SRI
618+
// integrity computed from those bytes (see trunk-rs/trunk#1028). wasm-bindgen normalizes
619+
// the non-deterministic cargo wasm into deterministic output, so hashing the cargo input
620+
// instead would churn the wasm file name on every build even when the served wasm is
621+
// identical; and its JS output is itself non-deterministic for byte-identical wasm, so
622+
// keying the JS name off the wasm would decouple the name from the bytes SRI pins.
623+
let hashed_wasm_base = self.hashed_base(wasm_path.as_std_path()).await?;
624+
let hashed_js_base = self.hashed_base(js_loader_path.as_std_path()).await?;
625+
626+
let hashed_wasm_name =
627+
apply_data_target_path(format!("{hashed_wasm_base}_bg.wasm"), &self.target_path);
628+
let hashed_js_name =
629+
apply_data_target_path(format!("{hashed_js_base}.js"), &self.target_path);
616630
let ts_name = format!("{}.d.ts", self.name);
617631
let hashed_ts_name =
618-
apply_data_target_path(format!("{hashed_name}.d.ts"), &self.target_path);
632+
apply_data_target_path(format!("{hashed_js_base}.d.ts"), &self.target_path);
619633

620-
let js_loader_path = bindgen_out.join(&js_name);
621634
let js_loader_path_dist = self.cfg.staging_dist.join(&hashed_js_name);
622-
let wasm_name = format!("{}_bg.wasm", self.name);
623-
let wasm_path = bindgen_out.join(&wasm_name);
624635
let wasm_path_dist = self.cfg.staging_dist.join(&hashed_wasm_name);
625636

626-
let hashed_loader_name = self
627-
.loader_shim
628-
.then(|| apply_data_target_path(format!("{hashed_name}_loader.js"), &self.target_path));
629-
let loader_shim_path = hashed_loader_name
630-
.as_ref()
631-
.map(|m| self.cfg.staging_dist.join(m));
632-
633637
tracing::debug!(
634638
"copying {js_loader_path} to {}",
635639
js_loader_path_dist.display()
@@ -661,12 +665,7 @@ impl RustApp {
661665
.context("error copying TS files to stage dir")?;
662666
}
663667

664-
if let Some(ref m) = loader_shim_path {
665-
tracing::debug!("creating {}", m.display());
666-
let mut loader_f = fs::File::create(m)
667-
.await
668-
.context("error creating loader shim script")?;
669-
668+
if self.loader_shim {
670669
let shim = match self.wasm_bindgen_target {
671670
WasmBindgenTarget::Web => {
672671
format!("import init from './{hashed_js_name}';await init();")
@@ -679,14 +678,26 @@ impl RustApp {
679678
\"no-modules\"!"
680679
),
681680
};
682-
loader_f
683-
.write_all(shim.as_bytes())
681+
682+
// Hash the loader by its own content so its file name stays in lockstep with the
683+
// js/wasm names it embeds; keying it off either file's base alone would leave a
684+
// stale loader (404) when only the other file changed (see trunk-rs/trunk#1028).
685+
let loader_src = bindgen_out.join(format!("{}_loader.js", self.name));
686+
fs::write(&loader_src, shim.as_bytes())
684687
.await
685688
.context("error writing loader shim script")?;
686-
loader_f
687-
.flush()
689+
690+
let hashed_loader_base = self.hashed_base(loader_src.as_std_path()).await?;
691+
let hashed_loader_name = apply_data_target_path(
692+
format!("{hashed_loader_base}_loader.js"),
693+
&self.target_path,
694+
);
695+
let loader_shim_path = self.cfg.staging_dist.join(&hashed_loader_name);
696+
697+
tracing::debug!("creating {}", loader_shim_path.display());
698+
fs::copy(&loader_src, &loader_shim_path)
688699
.await
689-
.context("error writing loader shim script")?;
700+
.context("error copying loader shim script to stage dir")?;
690701
}
691702

692703
// Check for any snippets, and copy them over.
@@ -819,16 +830,16 @@ impl RustApp {
819830
})
820831
}
821832

822-
/// create a cache busting hashed name for the wasm file, if enabled.
823-
async fn hashed_wasm_base(&self, wasm: &Path) -> Result<String> {
833+
/// create a cache busting hashed name from the given artifact, if enabled.
834+
async fn hashed_base(&self, path: &Path) -> Result<String> {
824835
// Skip the hashed file name for workers as their file name must be named at runtime.
825836
// Therefore, workers use the Cargo binary name for file naming.
826837
if self.app_type == RustAppType::Worker {
827838
return Ok(self.name.clone());
828839
}
829840

830841
Ok(self
831-
.hashed(wasm)
842+
.hashed(path)
832843
.await?
833844
.map(|hashed| format!("{}-{hashed}", self.name))
834845
.unwrap_or_else(|| self.name.clone()))

0 commit comments

Comments
 (0)