From 80ee95a78562040a2374331753a5fab9fa44d78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Mon, 22 Jun 2026 14:07:50 +0200 Subject: [PATCH] build: split build.sh into --compile-only / --postprocess-only phases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The container compile produces an OPT-INDEPENDENT base wasm; the only opt-dependent work is the final 'wasm-opt -O$LEVEL' shrink in apply-asyncify (host post-process). Splitting the build at that boundary lets CI cache the expensive compile ONCE (shared across -O1/-O2) and re-run only the asyncify/-O tail per opt level. --compile-only: in-container compile → output/ base wasm (no post-process). --postprocess-only: pure host dyncall+finalize+asyncify+wasm-opt -O on the existing base (no container; get-wasm-opt self-provisions Binaryen). Default 'both' is unchanged. Phase flags are extracted before ARGS so they are not forwarded to inner build-.sh. bash -n + shellcheck -S error clean; extraction unit-checked incl. the empty-after-filter case. Enables the two-tier CI cache (opt-independent base + opt-specific final) so an -O2 demo release reuses main's compiled base and only runs asyncify+O2. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker/build.sh | 70 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/docker/build.sh b/docker/build.sh index 46db5e5..0196bdd 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -110,12 +110,39 @@ export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-kicad-wasm-${BRANCH_NAME}}" echo "Using Docker project: ${COMPOSE_PROJECT_NAME}" echo "Building app: ${APP_NAME}" +# Build phase (cache split). The container compile produces an OPT-INDEPENDENT +# base wasm; the only opt-DEPENDENT work is the final `wasm-opt -O$LEVEL` shrink +# inside the host post-process (apply-asyncify.sh). Splitting them lets CI cache +# the expensive compile once (shared across -O1/-O2) and re-run just the +# asyncify/-O tail per opt level — see .github/workflows/. +# (default) both — compile in-container, then host post-process. +# --compile-only — only the in-container compile → base wasm in output/. +# --postprocess-only — only the host post-process (dyncall + finalize + +# asyncify + wasm-opt -O$BINARYEN_OPT_LEVEL) on the +# existing output/ base wasm; NO container needed +# (get-wasm-opt.sh self-provisions Binaryen). +# Extracted here so they are NOT forwarded to the inner build-.sh scripts. +PHASE="both" +_FILTERED=() +for _arg in "$@"; do + case "$_arg" in + --compile-only) PHASE="compile" ;; + --postprocess-only) PHASE="postprocess" ;; + *) _FILTERED+=("$_arg") ;; + esac +done +set -- "${_FILTERED[@]+"${_FILTERED[@]}"}" + # Add -j 10 by default if no -j flag is given ARGS=("$@") if [[ ! " ${ARGS[*]} " =~ " -j " ]]; then ARGS+=("-j" "10") fi +# Container compile + source sync — skipped entirely for --postprocess-only, +# which is pure host work on the already-built base wasm in output/. +if [[ "$PHASE" != "postprocess" ]]; then + # Start container if not running docker compose -f docker/docker-compose.yml up -d @@ -154,6 +181,8 @@ if [ $sync_rc -ne 0 ] && [ $sync_rc -ne 24 ]; then echo "ERROR: source sync failed after retries (exit ${sync_rc})"; exit 1 fi +fi # end: container compile + sync guard ("$PHASE" != postprocess) + # Map an app name to its inner CMake build subdirectory. Most apps share their # subdir name with the app name; pcb_calculator emits OUTPUT_NAME=calculator # but lives under the pcb_calculator/ subtree. @@ -301,16 +330,44 @@ pipeline_wait_all() { } TOTAL_APPS="${#APPS[@]}" -if [[ "${KICAD_PIPELINE:-0}" == "1" ]] && [ "$TOTAL_APPS" -gt 1 ]; then + +# Shared pipeline trap: on a failure, kill orphaned background wasm-opt jobs +# (each ~30 GB) and keep the monitor's done/fail marker from the EXIT trap. +_install_pipeline_trap() { + trap '_rc=$?; for p in "${PIPELINE_PIDS[@]}"; do kill "$p" 2>/dev/null || true; done; if [ $_rc -eq 0 ]; then kw_done; else kw_fail $_rc; fi' EXIT +} + +if [[ "$PHASE" == "compile" ]]; then + # --compile-only: produce the opt-independent base wasm; no host post-process. + idx=1 + for app in "${APPS[@]}"; do + compile_app "$app" "$idx" "$TOTAL_APPS" + idx=$((idx + 1)) + done +elif [[ "$PHASE" == "postprocess" ]]; then + # --postprocess-only: pure host post-process on the existing output/ base + # wasm (no container). Parallelize across apps when pipelining. + if [[ "${KICAD_PIPELINE:-0}" == "1" ]] && [ "$TOTAL_APPS" -gt 1 ]; then + mkdir -p "$PIPELINE_LOG_DIR" + ./scripts/common/get-wasm-opt.sh >/dev/null # pre-warm Binaryen once + _install_pipeline_trap + for app in "${APPS[@]}"; do + pipeline_postprocess "$app" + done + pipeline_wait_all + else + for app in "${APPS[@]}"; do + postprocess_app "$app" + done + fi +elif [[ "${KICAD_PIPELINE:-0}" == "1" ]] && [ "$TOTAL_APPS" -gt 1 ]; then + # both, pipelined: overlap app[i+1]'s container compile with app[i]'s host + # post-process (KICAD_PIPELINE=1). mkdir -p "$PIPELINE_LOG_DIR" # Pre-warm the Binaryen download once — two concurrent postprocesses racing # the first download would collide on the extract/mv. ./scripts/common/get-wasm-opt.sh >/dev/null - # If a compile fails, set -e aborts the script — don't leave orphaned - # wasm-opt jobs chewing 30 GB in the background. Keep the monitor's - # done/fail marker from the original EXIT trap (killing finished pids is a - # no-op, so this trap is safe on the success path too). - trap '_rc=$?; for p in "${PIPELINE_PIDS[@]}"; do kill "$p" 2>/dev/null || true; done; if [ $_rc -eq 0 ]; then kw_done; else kw_fail $_rc; fi' EXIT + _install_pipeline_trap idx=1 for app in "${APPS[@]}"; do compile_app "$app" "$idx" "$TOTAL_APPS" @@ -319,6 +376,7 @@ if [[ "${KICAD_PIPELINE:-0}" == "1" ]] && [ "$TOTAL_APPS" -gt 1 ]; then done pipeline_wait_all else + # both, sequential. idx=1 for app in "${APPS[@]}"; do compile_app "$app" "$idx" "$TOTAL_APPS"