From 5ef70c7284620c9e4ed475ea26f2e97b5ab45d03 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 25 Sep 2026 09:42:27 +0200 Subject: [PATCH] :sparkles: Serialize render-wasm builds per checkout (#11903) Protect shared setup, compilation, artifact copy, and target cleanup with one flock lock per checkout. Route watch builds and frontend cleanup through the protected scripts. Document the lock contract and normalize the frontend and exporter build:wasm commands. Closes #11901 AI-assisted-by: Space Bunny Free --- .serena/memories/render-wasm/core.md | 11 ++++-- exporter/package.json | 1 - frontend/package.json | 2 +- render-wasm/.gitignore | 1 + render-wasm/README.md | 30 +++++++++++++++- render-wasm/_build_lock | 53 ++++++++++++++++++++++++++++ render-wasm/build | 40 ++++++++++++--------- render-wasm/clean | 31 ++++++++++++++++ render-wasm/test | 10 +++--- render-wasm/watch | 22 +++++++----- 10 files changed, 166 insertions(+), 35 deletions(-) create mode 100644 render-wasm/_build_lock create mode 100755 render-wasm/clean diff --git a/.serena/memories/render-wasm/core.md b/.serena/memories/render-wasm/core.md index 17345b7a7d..c5d664983a 100644 --- a/.serena/memories/render-wasm/core.md +++ b/.serena/memories/render-wasm/core.md @@ -20,11 +20,18 @@ `./build` sources `_build_env`, which sets the Emscripten paths and `EMCC_CFLAGS`. The WASM heap starts at 256 MB and uses geometric growth. +- Linux builds require `flock` (util-linux). +- `build`, target cleanup, and each watch rebuild share `render-wasm/.render-wasm-build.lock` per checkout. The dispatcher for both targets does not hold the lock in its parent process. +- The lock covers setup, Cargo build, and artifact copy. The watch process releases it while waiting for changes. The operating system releases it on success, error, or signal; the file remains. +- The lock only coordinates repository scripts. Manual Cargo commands do not take it. +- Set `RENDER_WASM_LOCK_FILE` to the same path only when separate checkouts intentionally share a `CARGO_TARGET_DIR`. + ## Commands From `render-wasm/`: -- Build/copy frontend artifacts: `./build`. -- Watch rebuild: `./watch`. +- Build/copy frontend artifacts: `./build [frontend|export]`; no target builds frontend, then export. +- Clean one target: `./clean [frontend|export]`; never run root `cargo clean` on the shared `target/`. +- Watch rebuild: `./watch [frontend|export]`; its initial and change-triggered builds use `./build`. - Rust tests: `./test` or `cargo test `. - Cross-cutting testing principles and anti-patterns: `mem:testing`. - Lint: `./lint`. diff --git a/exporter/package.json b/exporter/package.json index f5a0930de0..2f5ad006f5 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -30,7 +30,6 @@ }, "scripts": { "clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target", - "build:wasm": "exit 0", "watch:app": "pnpm run build:wasm && pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch main", "watch": "pnpm run watch:app", "build:app": "clojure -M:dev:shadow-cljs release main", diff --git a/frontend/package.json b/frontend/package.json index 874ad8d232..4d5a5623e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,7 +44,7 @@ "watch:app:libs": "node ./scripts/build-libs.js --watch", "watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook", "clear:shadow-cache": "rm -rf .shadow-cljs", - "clear:wasm": "cargo clean --manifest-path ../render-wasm/Cargo.toml", + "clear:wasm": "../render-wasm/clean frontend", "watch": "exit 0", "watch:app": "pnpm run clear:shadow-cache && pnpm run clear:wasm && pnpm run build:wasm && concurrently --kill-others-on-fail \"pnpm run watch:app:assets\" \"pnpm run watch:app:main\" \"pnpm run watch:app:libs\"", "watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 3451 -h 0.0.0.0 --no-open\" \"node ./scripts/watch-storybook.js\"", diff --git a/render-wasm/.gitignore b/render-wasm/.gitignore index 391ed4d660..38aa215fb6 100644 --- a/render-wasm/.gitignore +++ b/render-wasm/.gitignore @@ -1,5 +1,6 @@ target/ debug/ +.render-wasm-build.lock **/*.rs.bk diff --git a/render-wasm/README.md b/render-wasm/README.md index 4ed4830bcd..a6e0c1e25a 100644 --- a/render-wasm/README.md +++ b/render-wasm/README.md @@ -27,7 +27,7 @@ cd penpot/render-wasm You can also use `./watch` to run the build on every change. -The build script will compile the project and copy the `.js` and `.wasm` files to their correct location within the frontend app. +The build script compiles the project and copies the `.js` and `.wasm` files to the app that uses each target. ### Render targets @@ -52,6 +52,34 @@ Each target keeps its own `CARGO_TARGET_DIR` (`target/`), so switching between them does not invalidate the other's cache. Set `BUILD_MODE=release` (or `NODE_ENV=production`) for an optimized build; the default is `debug`. +### Serialize builds in one checkout + +All targets in one checkout share one `flock` lock stored at +`render-wasm/.render-wasm-build.lock`. The lock covers dependency setup, the +Cargo build, artifact copy, and target cleanup. If another build or cleanup +already holds the lock, the new process prints a waiting message and starts +only after the first process exits. + +The watch command takes the lock for each build, then releases it while it +waits for source changes. The lock file stays in the checkout after a build, +but the operating system releases its lock when the process exits, including +after an error or signal. + +The lock applies to one checkout and only to commands that use these scripts. +A manual Cargo build can still write to the same target without taking the +lock. Set `RENDER_WASM_LOCK_FILE` to the same path in each process when you +make different checkouts share a `CARGO_TARGET_DIR`. The supported Linux build +environment must provide `flock` from util-linux; `flock --version` checks this +dependency. + +Use the target cleanup commands instead of running `cargo clean` on the shared +`target/` directory: + +```sh +./clean frontend # remove target/frontend only +./clean export # remove target/export only +``` + Each target writes its own generated `shared.js` (the enum discriminants the CLJS side compiles against) next to the code that imports it — respectively `frontend/src/app/render_wasm/api/shared.js` and diff --git a/render-wasm/_build_lock b/render-wasm/_build_lock new file mode 100644 index 0000000000..68107cd997 --- /dev/null +++ b/render-wasm/_build_lock @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +_render_wasm_lock_source=${BASH_SOURCE[0]} +_render_wasm_lock_dir=${_render_wasm_lock_source%/*} +if [[ "$_render_wasm_lock_dir" == "$_render_wasm_lock_source" ]]; then + _render_wasm_lock_dir="." +fi +_RENDER_WASM_LOCK_DIR=$(cd -- "$_render_wasm_lock_dir" && pwd) +_RENDER_WASM_BUILD_LOCK_HELD=0 + +release_build_lock() { + if [[ "$_RENDER_WASM_BUILD_LOCK_HELD" == "1" ]]; then + _RENDER_WASM_BUILD_LOCK_HELD=0 + flock --unlock 9 2>/dev/null || true + fi + + exec 9>&- +} + +acquire_build_lock() { + if ! command -v flock > /dev/null 2>&1; then + echo "ERROR: render-wasm builds require flock from util-linux on Linux" >&2 + return 1 + fi + + if [[ "$_RENDER_WASM_BUILD_LOCK_HELD" == "1" ]]; then + return 0 + fi + + local lock_file="${RENDER_WASM_LOCK_FILE:-$_RENDER_WASM_LOCK_DIR/.render-wasm-build.lock}" + + if ! exec 9>> "$lock_file"; then + echo "ERROR: cannot open render-wasm build lock: $lock_file" >&2 + return 1 + fi + + trap 'release_build_lock' EXIT + trap 'release_build_lock; exit 129' HUP + trap 'release_build_lock; exit 130' INT + trap 'release_build_lock; exit 131' QUIT + trap 'release_build_lock; exit 143' TERM + + if ! flock --exclusive --nonblock 9; then + echo "Waiting for another render-wasm build to finish: $lock_file" >&2 + if ! flock --exclusive 9; then + echo "ERROR: cannot acquire render-wasm build lock: $lock_file" >&2 + release_build_lock + return 1 + fi + fi + + _RENDER_WASM_BUILD_LOCK_HELD=1 +} diff --git a/render-wasm/build b/render-wasm/build index 2a832fc3f4..7b73aaf5a7 100755 --- a/render-wasm/build +++ b/render-wasm/build @@ -5,32 +5,40 @@ # With no target, builds both. Set BUILD_MODE=release (or NODE_ENV=production) # for an optimized build. See `_build_env` for what each target changes. -_SCRIPT_DIR=$(dirname $0); +_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +_RENDER_TARGET=${1:-} -# Each target needs its own `_build_env`, so re-enter per target. -case "${1:-}" in - frontend|export) +case "$_RENDER_TARGET" in + frontend|export) ;; + "") + # Each target needs its own `_build_env`, so re-enter per target. + for _target in frontend export; do + "$_SCRIPT_DIR/build" "$_target" "$@" || exit $? + done + exit 0 ;; *) - for _target in frontend export; do - "$_SCRIPT_DIR/build" "$_target" "$@" || exit $?; - done - exit 0; + echo "ERROR: unknown render target '$_RENDER_TARGET' (expected 'frontend' or 'export')" >&2 + exit 1 ;; esac +set -e + EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh -pushd $_SCRIPT_DIR; +pushd "$_SCRIPT_DIR" > /dev/null -. ./_build_env +export RENDER_TARGET="$_RENDER_TARGET" +. ./_build_env "$_RENDER_TARGET" +. ./_build_lock -set -ex; +acquire_build_lock -setup; -build; -copy_target_artifacts; +set -x -exit $?; +setup +build +copy_target_artifacts -popd +popd > /dev/null diff --git a/render-wasm/clean b/render-wasm/clean new file mode 100755 index 0000000000..f2165cff73 --- /dev/null +++ b/render-wasm/clean @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# Usage: ./clean [frontend|export] + +set -e + +_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +_RENDER_TARGET=${1:-frontend} + +case "$_RENDER_TARGET" in + frontend|export) ;; + *) + echo "ERROR: unknown render target '$_RENDER_TARGET' (expected 'frontend' or 'export')" >&2 + exit 1 + ;; +esac + +pushd "$_SCRIPT_DIR" > /dev/null + +export RENDER_TARGET="$_RENDER_TARGET" +unset CARGO_TARGET_DIR +. ./_build_env "$_RENDER_TARGET" +. ./_build_lock + +acquire_build_lock +cargo clean --target-dir "$CARGO_TARGET_DIR" + +RESULT=$? +popd > /dev/null + +exit $RESULT diff --git a/render-wasm/test b/render-wasm/test index 8019b1fecd..a94cf8025d 100755 --- a/render-wasm/test +++ b/render-wasm/test @@ -4,14 +4,14 @@ set -x export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"} export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"x86_64-unknown-linux-gnu"}; -_SCRIPT_DIR=$(dirname $0); -pushd $_SCRIPT_DIR; +_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +pushd "$_SCRIPT_DIR" > /dev/null . ./_build_env frontend cargo test --bin render_wasm -- --show-output -# Exit with the same status code as cargo test -exit $? +RESULT=$? +popd > /dev/null -popd +exit $RESULT diff --git a/render-wasm/watch b/render-wasm/watch index 1825bf7667..f9ad114bb8 100755 --- a/render-wasm/watch +++ b/render-wasm/watch @@ -2,22 +2,26 @@ # Usage: ./watch [frontend|export] -_SCRIPT_DIR=$(dirname $0); -pushd $_SCRIPT_DIR; +set -e -. ./_build_env +_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +pushd "$_SCRIPT_DIR" > /dev/null + +_RENDER_TARGET=${1:-${RENDER_TARGET:-frontend}} +export RENDER_TARGET="$_RENDER_TARGET" +. ./_build_env "$_RENDER_TARGET" "${@:2}" + +# The initial build uses the protected build entry point. Its lock is released +# before cargo watch starts waiting for source changes. +"$_SCRIPT_DIR/build" "$RENDER_TARGET" $CARGO_PARAMS set -x -build; -copy_target_artifacts; - -pushd $_SCRIPT_DIR; cargo watch \ --why \ -i "_tmp*" \ -x "build $CARGO_PARAMS" \ - -s "./build $RENDER_TARGET" \ + -s "\"$_SCRIPT_DIR/build\" \"$RENDER_TARGET\" $CARGO_PARAMS" \ -s "echo 'DONE\n'"; -popd +popd > /dev/null