1use std::collections::BTreeSet;2use std::env;3use std::fs;4use std::path::{Component, Path};56const PORTAL_PAYLOAD_MANIFEST: &str = "runtime-files.txt";78fn main() {9 assemble_portal_dist();10 tauri_build::build()11}1213/// Assemble the Portal webview payload (`../portal-dist`) from an explicit14/// runtime allowlist before the crate compiles.15///16/// `tauri::generate_context!` resolves `frontendDist` at compile time and17/// `portal-dist/` is generated, not committed, so every build path — plain18/// `cargo`, `cargo tauri`, and rust-analyzer — needs the directory to exist19/// without an external shell step (the previous `beforeBuildCommand` hook only20/// ran under the Tauri CLI and depended on `bash`). Only the file classes named21/// here can reach the signed bundle; `*.test.mjs` is excluded by construction.22fn assemble_portal_dist() {23 let manifest_dir =24 env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set for build scripts");25 let source_dir = Path::new(&manifest_dir).join("../portal");26 let dist_dir = Path::new(&manifest_dir).join("../portal-dist");2728 let manifest_path = source_dir.join(PORTAL_PAYLOAD_MANIFEST);29 let manifest = fs::read_to_string(&manifest_path)30 .expect("portal runtime payload manifest must be readable");31 let mut wanted = BTreeSet::new();32 for (line_index, raw_name) in manifest.lines().enumerate() {33 let line_number = line_index + 1;34 let name = raw_name.trim();35 if name.is_empty() || name.starts_with('#') {36 continue;37 }38 if name != raw_name {39 panic!("portal runtime payload manifest line {line_number} has surrounding whitespace");40 }41 let path = Path::new(name);42 if !matches!(43 path.components().collect::<Vec<_>>().as_slice(),44 [Component::Normal(_)]45 ) {46 panic!(47 "portal runtime payload manifest line {line_number} is not one top-level file: {name}"48 );49 }50 if name.ends_with(".test.mjs") {51 panic!(52 "portal runtime payload manifest line {line_number} includes a test suite: {name}"53 );54 }55 if !wanted.insert(name.to_owned()) {56 panic!("portal runtime payload manifest repeats {name}");57 }58 }59 if wanted.is_empty() {60 panic!("portal runtime payload manifest must name at least one file");61 }62 for name in &wanted {63 let metadata = fs::symlink_metadata(source_dir.join(name)).unwrap_or_else(|error| {64 panic!("allowlisted portal payload file {name} is missing: {error}")65 });66 if !metadata.file_type().is_file() {67 panic!("allowlisted portal payload file is missing: {name}");68 }69 }7071 fs::create_dir_all(&dist_dir).expect("portal-dist directory is creatable");7273 // Copy the allowlist, overwriting stale content, and rebuild whenever any74 // staged source changes.75 for name in &wanted {76 fs::copy(source_dir.join(name), dist_dir.join(name))77 .unwrap_or_else(|error| panic!("failed to stage portal payload {name}: {error}"));78 println!("cargo:rerun-if-changed=../portal/{name}");79 }8081 // Prune anything previously staged that is no longer allowlisted.82 for entry in fs::read_dir(&dist_dir).expect("portal-dist directory is readable") {83 let entry = entry.expect("readable portal-dist entry");84 let name = entry.file_name().to_string_lossy().into_owned();85 if !wanted.contains(&name) {86 let file_type = entry87 .file_type()88 .expect("portal-dist entry type is readable");89 if file_type.is_dir() {90 fs::remove_dir_all(entry.path()).unwrap_or_else(|error| {91 panic!("failed to prune portal-dist directory {name}: {error}")92 });93 } else {94 fs::remove_file(entry.path()).unwrap_or_else(|error| {95 panic!("failed to prune portal-dist file {name}: {error}")96 });97 }98 }99 }100101 // Watch the directory itself so added or removed source files re-run this.102 println!("cargo:rerun-if-changed=../portal/{PORTAL_PAYLOAD_MANIFEST}");103 println!("cargo:rerun-if-changed=../portal");104}