diff --git a/.gitignore b/.gitignore index 0dae266..b59fb20 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ result result-* .buildkite/.issue-poll-state /host/gleam/slash/build/ +# Staging tree used by scripts/sync-flake-path-deps.sh +/.deps-sync/ /host/target/ diff --git a/host/build.rs b/host/build.rs index a831010..76ecd80 100644 --- a/host/build.rs +++ b/host/build.rs @@ -1,4 +1,10 @@ //! Compile Gleam Wasm guests into OUT_DIR for `include_bytes!`. +//! +//! Prefers a live `gleam` binary (GLEAM env or well-known paths). When that is +//! unavailable — e.g. hermetic `nix build .#sleek` / Flatpak — falls back to +//! the checked-in `gleam/slash/prebuilt/gleam_slash.wasm` so offline CI still +//! links. Regenerate the prebuilt with a wasm-capable gleam: +//! https://tangled.org/nandi.uk/gleam (branch wasm) use std::env; use std::fs; @@ -8,39 +14,85 @@ use std::process::Command; fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - let gleam = find_gleam(&manifest_dir); - eprintln!("sleek-host build.rs: gleam → {}", gleam.display()); println!("cargo:rerun-if-env-changed=GLEAM"); build_guest( - &gleam, &manifest_dir, &out_dir, "gleam_slash", "gleam/slash", "src/gleam_slash.gleam", + "gleam/slash/prebuilt/gleam_slash.wasm", ); } fn build_guest( - gleam: &Path, manifest_dir: &Path, out_dir: &Path, package: &str, guest_rel: &str, src_rel: &str, + prebuilt_rel: &str, ) { let guest_dir = manifest_dir.join(guest_rel); let guest_src = guest_dir.join(src_rel); + let prebuilt = manifest_dir.join(prebuilt_rel); println!("cargo:rerun-if-changed={}", guest_src.display()); println!( "cargo:rerun-if-changed={}", guest_dir.join("gleam.toml").display() ); + println!("cargo:rerun-if-changed={}", prebuilt.display()); + let wasm_src = match find_gleam(manifest_dir) { + Some(gleam) => { + eprintln!("sleek-host build.rs: gleam → {}", gleam.display()); + run_gleam_build(&gleam, &guest_dir, package) + } + None => { + if !prebuilt.is_file() { + panic!( + "gleam not found and prebuilt wasm missing at {}.\n\ + Set GLEAM to a wasm-capable gleam binary \ + (https://tangled.org/nandi.uk/gleam , branch wasm),\n\ + or commit {prebuilt_rel} under host/.", + prebuilt.display() + ); + } + eprintln!( + "sleek-host build.rs: gleam unavailable; using prebuilt {}", + prebuilt.display() + ); + prebuilt + } + }; + + let wasm_dst = out_dir.join(format!("{package}.wasm")); + fs::copy(&wasm_src, &wasm_dst).unwrap_or_else(|err| { + panic!( + "copy {} → {}: {err}", + wasm_src.display(), + wasm_dst.display() + ); + }); + + let inspect = manifest_dir.join("target").join(format!("{package}.wasm")); + if let Some(parent) = inspect.parent() { + let _ = fs::create_dir_all(parent); + } + let _ = fs::copy(&wasm_src, &inspect); + + eprintln!( + "sleek-host build.rs: {package} ready ({} bytes) → {}", + fs::metadata(&wasm_dst).map(|m| m.len()).unwrap_or(0), + wasm_dst.display() + ); +} + +fn run_gleam_build(gleam: &Path, guest_dir: &Path, package: &str) -> PathBuf { let status = Command::new(gleam) - .current_dir(&guest_dir) + .current_dir(guest_dir) .arg("build") .status() .unwrap_or_else(|err| { @@ -74,34 +126,14 @@ fn build_guest( wasm_src.display() ); } - - let wasm_dst = out_dir.join(format!("{package}.wasm")); - fs::copy(&wasm_src, &wasm_dst).unwrap_or_else(|err| { - panic!( - "copy {} → {}: {err}", - wasm_src.display(), - wasm_dst.display() - ); - }); - - let inspect = manifest_dir.join("target").join(format!("{package}.wasm")); - if let Some(parent) = inspect.parent() { - let _ = fs::create_dir_all(parent); - } - let _ = fs::copy(&wasm_src, &inspect); - - eprintln!( - "sleek-host build.rs: {package} ready ({} bytes) → {}", - fs::metadata(&wasm_dst).map(|m| m.len()).unwrap_or(0), - wasm_dst.display() - ); + wasm_src } -fn find_gleam(manifest_dir: &Path) -> PathBuf { +fn find_gleam(manifest_dir: &Path) -> Option { if let Ok(path) = env::var("GLEAM") { let p = PathBuf::from(path); if p.is_file() { - return p; + return Some(p); } panic!("GLEAM={p:?} is not a file"); } @@ -117,9 +149,14 @@ fn find_gleam(manifest_dir: &Path) -> PathBuf { } for candidate in candidates { if candidate.is_file() { - return candidate.canonicalize().unwrap_or(candidate); + return Some(candidate.canonicalize().unwrap_or(candidate)); } } - PathBuf::from("gleam") + // PATH lookup — only accept if `gleam --version` works. + let path_candidate = PathBuf::from("gleam"); + match Command::new(&path_candidate).arg("--version").status() { + Ok(status) if status.success() => Some(path_candidate), + _ => None, + } } diff --git a/host/gleam/slash/README.md b/host/gleam/slash/README.md index 3d29724..c981941 100644 --- a/host/gleam/slash/README.md +++ b/host/gleam/slash/README.md @@ -16,3 +16,16 @@ nix develop --command just gleam-slash ``` Compares Gleam output to `sleek::slash::parse_native` for the same cases. + +## Prebuilt Wasm + +`host/build.rs` falls back to `prebuilt/gleam_slash.wasm` when a wasm-capable +`gleam` binary is not available (hermetic `nix build .#sleek` / Flatpak CI). +Regenerate after editing `src/gleam_slash.gleam`: + +```bash +export GLEAM=/path/to/nandi.uk/gleam/target/release/gleam # branch wasm +(cd host/gleam/slash && "$GLEAM" build) +cp host/gleam/slash/build/dev/wasm/gleam_slash/gleam_slash.wasm \ + host/gleam/slash/prebuilt/gleam_slash.wasm +``` diff --git a/host/gleam/slash/prebuilt/gleam_slash.wasm b/host/gleam/slash/prebuilt/gleam_slash.wasm new file mode 100644 index 0000000000000000000000000000000000000000..dd4419494444f08ef238f03bd3fef3c30cf491e0 GIT binary patch literal 2274 zcmZQbEY4+QU|?YEW|T@`s;^_LOJJ_At7EKBV62A_b#PXF0s}+{AqizM)k76SlrYyL z_%LJYA?6~RhL8fYAclZRCI=SQ;+)jfZ1$welGOOb5(WkcpS3tAGdY!k5yIol$uCbW zN=__JWnhAcu;rxYrI%zdFf)sbv9PkSb8vET^YHTV3kV7ci-?MeOGvUZGcqu;u`|{) zI<_!!v+rdV5l_!aP0Woi&PgoJh>tJHFUU-eFG;N^VPHX(Nh!)NfT&_%MU@FiEGltK zF3BuQWndFSF)b@UGY?`IyC8~$UuJSP0|!5fm~ScrC#v>>#G>NV_}t=j1}*_qrN!wC z+#)DKxv3C~c!W@dU2_XcDj9f1Q3O*miwhDIF*%h%2-VwO`I&hP!l-@;N-Sp(LA4Izj^x}F22oV& zzyZP_#+I9!n_pDP#K1L~Tg-}yvCg4hfkA*-kx_xsi6M}s-jPK>T#-qE$&tZ~i9vyh z)saI$T#;FU*^w2*XI5Y^V`5QY=H@nGVnWxdz`&}&;KU%n0MhTIAIMU#z~ICPGF^a? zRe^zb<6v#M``xq4%9hpILj1c!Ra-&GGphzue zWMZsy!QyK+1vZGU*;o}A1Vj~?6c_~f6`2*71el!|LKRsQSR5JMSn3s+A$(Q^Rxsa@ zM?hSWU4h+^9b^r=0uv~N*cF(~m^c*JL5djMpux(azyYyrF(a&s$!yazHI6dhp0nIPU{QeXrb&O}n!fdc^%b|_H+j%x<)c}$G%^$HA*M;HVo z!Ep}uE~5ZDs{%9Fp$ZI+XFzhWpa3TpE(I<~Tynwuqrl*J1*8n*d5{rcW!wtf5M|sT zVUV{K7#tsfl!3w>q)dTPK!6n#2cSS^aC`xh1BEM8j)zr&9p;J;AURNQLgiRkK|u@A z_Xi{g3QedS7b_@aL2&>|Dm)52j%=Wa;(#L{g94*~5IC*ZgEGzm zQ1Vw`a$>AeU=(2FCMj~ji5C$$D5)2m@!$#Dg_*G)oM{;qn1W%ERHp!P3j-*LIw=IQ z)G2`Sn*t-qDNF)F+|cN&bE;=$a02B+6!XEE3T{4%6gZ&ZQkwX@56%1vOdzkjF|$A% z$soWADp|mpi4mHam_W`HU{+*RU`3QTpfmtZg>0+}EDFpDi~@Yz+~SOk^~wy2jLHlO z3|WrMS&lV@DhvvY+_j9LqD6xVRM0SjC}ssF1?DVAmMj5rkl|pS854segCZ-lg957p zlP8OV0*eA`mI8wUYnCnpixmS4DEDS5vhpx;bHmj!L)0;4fs`_0Q>wti9mB{}&r}Zz zaW@tRM@Ak-ZUqJnCI%%Y1&~|ZSsWZ0co;!uGBZ0UFe@b1_p+l)Wot>1_lO@ c7^rSyVqjp%O=VyJ$$@Kd1_lOZkXUg#05UL/dev/null || true + return 0 + fi + + # Fast path: fully writable tree — wipe and copy in place. + if [[ -d "$dest" && -w "$dest" ]] && touch "$dest/.sync-write-test" 2>/dev/null; then + rm -f "$dest/.sync-write-test" + if find "$dest" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null \ + && cp -a "$staging/." "$dest/" \ + && chmod -R u+w "$dest" 2>/dev/null; then + return 0 + fi + log "in-place sync of ${name} failed; trying atomic replace" + fi + + # Atomic replace via rename (works when nested files aren't writable but the + # parent directory is). + if [[ -w "$parent" ]]; then + tmp="${dest}.new.$$" + old="${dest}.old.$$" + rm -rf "$tmp" "$old" + cp -a "$staging" "$tmp" + if mv "$dest" "$old" 2>/dev/null; then + if mv "$tmp" "$dest"; then + rm -rf "$old" 2>/dev/null || log "warning: left behind ${old} (could not delete)" + chmod -R u+w "$dest" 2>/dev/null || true + return 0 + fi + mv "$old" "$dest" 2>/dev/null || true + rm -rf "$tmp" + else + rm -rf "$tmp" + fi + fi + + if expected_marker "$name" "$dest"; then + log "warning: ${dest} not fully writable; reusing existing checkout" + return 0 + fi + + log "cannot sync ${name} → ${dest} (not writable and no usable checkout)" + return 1 +} + sync_input() { local name="$1" input="$2" local dest="$WORKSPACES/$name" - local src rev + local src rev staging src="$(flake_input_path "$input")" rev="$(jq -r ".nodes.${input}.locked.rev" "$ROOT/flake.lock")" log "syncing ${name} (${input} @ ${rev:0:7}) → ${dest}" - mkdir -p "$dest" - # Sibling dirs under / (e.g. /vidya on Codespaces) may not be removable; sync in place. - find "$dest" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true - cp -a "$src/." "$dest/" - chmod -R u+w "$dest" + + staging="$ROOT/.deps-sync/${name}" + rm -rf "$staging" + mkdir -p "$staging" + cp -a "$src/." "$staging/" + chmod -R u+w "$staging" + if [[ "$input" == "vidya" && -f "$ROOT/patches/vidya-android-winit.patch" ]]; then # Best-effort: Radicle tip may not need / match this GitHub-era patch. - if ! patch -p1 -d "$dest" --forward --batch \ + if ! patch -p1 -d "$staging" --forward --batch \ < "$ROOT/patches/vidya-android-winit.patch" >/dev/null; then log "vidya-android-winit.patch did not apply (ok if tip has no video/winit gap)" - rm -f "$dest/Cargo.toml.rej" "$dest"/*.rej 2>/dev/null || true + rm -f "$staging/Cargo.toml.rej" "$staging"/*.rej 2>/dev/null || true fi fi + + install_sibling "$name" "$dest" "$staging" } want() {