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