diff --git a/render-wasm/_build_env b/render-wasm/_build_env index 11c192fbb2..ce5e34d1d5 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -2,7 +2,7 @@ export RUST_VERSION=1.99.0 -export VERSION_TAG=${VERSION:-develop}; +export VERSION_TAG="${VERSION_TAG:-${VERSION:-develop}}"; export RENDER_TARGET="${RENDER_TARGET:-${1:-frontend}}"; case "$RENDER_TARGET" in @@ -13,15 +13,15 @@ case "$RENDER_TARGET" in ;; esac -if [ "$NODE_ENV" = "production" ]; then - export BUILD_MODE="release"; +if [ "${NODE_ENV:-}" = "production" ]; then + export BUILD_MODE="${BUILD_MODE:-release}"; else export BUILD_MODE=${BUILD_MODE:-debug}; fi export BUILD_NAME="${BUILD_NAME:-render-wasm}" -export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; -export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"target/$RENDER_TARGET"}; +export CARGO_BUILD_TARGET="${CARGO_BUILD_TARGET:-wasm32-unknown-emscripten}"; +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-target/$RENDER_TARGET}"; # Skia prebuild with wasm SIMD and Wasm EH/SjLj (-ljmp-wasm). Must match # EMCC_CFLAGS below. Override with SKIA_BINARIES_URL when needed. export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.153.3/skia-binaries-b7f043e0b1e2a850e702-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp-pdf-simd-ljmp-wasm.tar.gz"} @@ -75,43 +75,82 @@ export EMCC_CFLAGS="--no-entry \ export EM_CACHE="/tmp/emsdk_cache"; -export CARGO_PARAMS="${@:2}"; - +CARGO_ARGS=("${@:2}"); +_CARGO_PROFILE_SET=0; export CARGO_PROFILE_DIR="debug"; -if [ "$BUILD_MODE" = "profiling" ]; then - # Optimized like release, but keeps the wasm name section so the Firefox - # profiler can resolve Rust frames (release strips them). `--profiling-funcs` - # preserves function names through wasm-opt without pulling in DWARF. - export CARGO_PARAMS="--profile profiling $CARGO_PARAMS"; - export CARGO_PROFILE_DIR="profiling"; - export EMCC_CFLAGS="-O3 -sASSERTIONS=0 --profiling-funcs $EMCC_CFLAGS"; -elif [ "$BUILD_MODE" = "release" ]; then - case "$RENDER_TARGET" in - frontend) - export CARGO_PARAMS="--release $CARGO_PARAMS"; +for (( _cargo_index=0; _cargo_index < ${#CARGO_ARGS[@]}; _cargo_index++ )); do + case "${CARGO_ARGS[_cargo_index]}" in + --release|-r) + _CARGO_PROFILE_SET=1; export CARGO_PROFILE_DIR="release"; - export EMCC_CFLAGS="-O3 -sASSERTIONS=0 $EMCC_CFLAGS"; ;; - export) - export CARGO_PARAMS="--profile size $CARGO_PARAMS"; - export CARGO_PROFILE_DIR="size"; - export EMCC_CFLAGS="-Oz -sASSERTIONS=0 $EMCC_CFLAGS"; + --profile) + _CARGO_PROFILE_SET=1; + export CARGO_PROFILE_DIR="${CARGO_ARGS[_cargo_index + 1]}"; + ;; + --profile=*) + _CARGO_PROFILE_SET=1; + export CARGO_PROFILE_DIR="${CARGO_ARGS[_cargo_index]#--profile=}"; + ;; + --target-dir) + export CARGO_TARGET_DIR="${CARGO_ARGS[_cargo_index + 1]:-$CARGO_TARGET_DIR}"; + ;; + --target-dir=*) + export CARGO_TARGET_DIR="${CARGO_ARGS[_cargo_index]#--target-dir=}"; ;; esac -else - # TODO: Extra parameters that could be good to look into: - # -gseparate-dwarf - # -gsplit-dwarf - # -gsource-map - export EMCC_CFLAGS="-g -sASSERTIONS=1 -sVERBOSE=1 $EMCC_CFLAGS" +done + +if [ "$_CARGO_PROFILE_SET" = "0" ]; then + if [ "$BUILD_MODE" = "profiling" ]; then + CARGO_ARGS=(--profile profiling "${CARGO_ARGS[@]}"); + export CARGO_PROFILE_DIR="profiling"; + elif [ "$BUILD_MODE" = "release" ]; then + case "$RENDER_TARGET" in + frontend) + CARGO_ARGS=(--release "${CARGO_ARGS[@]}"); + export CARGO_PROFILE_DIR="release"; + ;; + export) + CARGO_ARGS=(--profile size "${CARGO_ARGS[@]}"); + export CARGO_PROFILE_DIR="size"; + ;; + esac + fi fi -export FRONTEND_DEST="../frontend/resources/public/js"; -export EXPORT_DEST="../exporter/resources/wasm"; +case "$CARGO_PROFILE_DIR" in + profiling) + # Keep the wasm name section so profilers can resolve Rust frames. + export EMCC_CFLAGS="-O3 -sASSERTIONS=0 --profiling-funcs $EMCC_CFLAGS"; + ;; + release) + export EMCC_CFLAGS="-O3 -sASSERTIONS=0 $EMCC_CFLAGS"; + ;; + size) + export EMCC_CFLAGS="-Oz -sASSERTIONS=0 $EMCC_CFLAGS"; + ;; + *) + # TODO: Extra parameters that could be good to look into: + # -gseparate-dwarf + # -gsplit-dwarf + # -gsource-map + export EMCC_CFLAGS="-g -sASSERTIONS=1 -sVERBOSE=1 $EMCC_CFLAGS"; + ;; +esac -export FRONTEND_SHARED_DEST="../frontend/src/app/render_wasm/api/shared.js"; -export EXPORT_SHARED_DEST="../exporter/src/app/wasm/shared.js"; +if [ "$CARGO_PROFILE_DIR" = "dev" ]; then + export CARGO_PROFILE_DIR="debug"; +fi + +export RENDER_WASM_BUILD_INFO="${RENDER_WASM_BUILD_INFO:-$CARGO_TARGET_DIR/build-info.json}"; + +export FRONTEND_DEST="${FRONTEND_DEST:-../frontend/resources/public/js}"; +export EXPORT_DEST="${EXPORT_DEST:-../exporter/resources/wasm}"; + +export FRONTEND_SHARED_DEST="${FRONTEND_SHARED_DEST:-../frontend/src/app/render_wasm/api/shared.js}"; +export EXPORT_SHARED_DEST="${EXPORT_SHARED_DEST:-../exporter/src/app/wasm/shared.js}"; function clean { cargo clean; @@ -130,29 +169,39 @@ function setup { } function build { - cargo build $CARGO_PARAMS; + ( + set -o pipefail; + cargo build --message-format=json-render-diagnostics "${CARGO_ARGS[@]}" \ + | node ./scripts/cargo-build.cjs "$RENDER_WASM_BUILD_INFO" "$PWD/Cargo.toml"; + ) } function copy_artifacts { - DEST=$1; - SRC="$CARGO_TARGET_DIR/$CARGO_BUILD_TARGET/$CARGO_PROFILE_DIR"; + local DEST="$1"; + local SRC; + SRC=$(jq -r '.artifact_dir' -- "$RENDER_WASM_BUILD_INFO") + mkdir -p -- "$DEST"; - mkdir -p $DEST; - - cp $SRC/render_wasm.js $DEST/$BUILD_NAME.js; - cp $SRC/render_wasm.wasm $DEST/$BUILD_NAME.wasm; - if [ -f $SRC/render_wasm.wasm.map ]; then - cp $SRC/render_wasm.wasm.map $DEST/$BUILD_NAME.wasm.map; + cp -- "$SRC/render_wasm.js" "$DEST/$BUILD_NAME.js"; + cp -- "$SRC/render_wasm.wasm" "$DEST/$BUILD_NAME.wasm"; + if [ -f "$SRC/render_wasm.wasm.map" ]; then + cp -- "$SRC/render_wasm.wasm.map" "$DEST/$BUILD_NAME.wasm.map"; fi - sed -i "s/render_wasm.wasm/$BUILD_NAME.wasm?version=$VERSION_TAG/g" $DEST/$BUILD_NAME.js; + # Rewrite the generated JavaScript so it loads the renamed Wasm file with + # a version query, such as render-wasm.wasm?version=develop. + local WASM_URL="$BUILD_NAME.wasm?version=$VERSION_TAG"; + WASM_URL=${WASM_URL//\\/\\\\}; + WASM_URL=${WASM_URL//&/\\&}; + WASM_URL=${WASM_URL//|/\\|}; + sed -i "s|render_wasm\\.wasm|$WASM_URL|g" "$DEST/$BUILD_NAME.js"; # The worker bundle is a browser concern; the exporter imports the ESM # module directly under Node. if [ "$RENDER_TARGET" = "frontend" ]; then - pnpm exec esbuild $SRC/render_wasm.js \ + pnpm exec esbuild "$SRC/render_wasm.js" \ --log-level=error \ - --outfile=$DEST/worker/render.js \ + "--outfile=$DEST/worker/render.js" \ --platform=neutral \ --format=iife \ --global-name=WasmModule; @@ -160,10 +209,11 @@ function copy_artifacts { } function copy_shared_artifact { - DEST=$1; - SHARED_FILE=$(find $CARGO_TARGET_DIR/$CARGO_BUILD_TARGET -name render_wasm_shared.js | head -n 1); - - cp $SHARED_FILE $DEST; + local DEST="$1"; + local SHARED_FILE; + SHARED_FILE=$(jq -r '.shared_file' -- "$RENDER_WASM_BUILD_INFO") + mkdir -p -- "$(dirname -- "$DEST")"; + cp -- "$SHARED_FILE" "$DEST"; } # Copies whatever the current RENDER_TARGET produced to where that target's @@ -179,4 +229,8 @@ function copy_target_artifacts { copy_shared_artifact "$EXPORT_SHARED_DEST"; ;; esac + + if [ -n "${RENDER_WASM_SHARED_ARTIFACT:-}" ]; then + copy_shared_artifact "$RENDER_WASM_SHARED_ARTIFACT"; + fi } diff --git a/render-wasm/build b/render-wasm/build index 7b73aaf5a7..c4061068f2 100755 --- a/render-wasm/build +++ b/render-wasm/build @@ -23,14 +23,14 @@ case "$_RENDER_TARGET" in ;; esac -set -e +set -e -o pipefail EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh pushd "$_SCRIPT_DIR" > /dev/null export RENDER_TARGET="$_RENDER_TARGET" -. ./_build_env "$_RENDER_TARGET" +. ./_build_env "$_RENDER_TARGET" "${@:2}" . ./_build_lock acquire_build_lock diff --git a/render-wasm/scripts/cargo-build.cjs b/render-wasm/scripts/cargo-build.cjs new file mode 100644 index 0000000000..035703a03c --- /dev/null +++ b/render-wasm/scripts/cargo-build.cjs @@ -0,0 +1,120 @@ +// Usage: cargo build --message-format=json-render-diagnostics ... | +// node scripts/cargo-build.cjs +const fs = require("node:fs"); +const path = require("node:path"); +const readline = require("node:readline"); + +async function main() { + const [infoFile, manifestFile] = process.argv.slice(2); + if (!infoFile || !manifestFile) { + throw new Error( + "Usage: cargo-build.cjs ", + ); + } + const manifest = path.resolve(manifestFile); + // A failed invocation must not leave facts from an earlier build. + fs.rmSync(infoFile, { force: true }); + + const artifacts = []; + const buildScripts = new Map(); + let succeeded = false; + const lines = readline.createInterface({ + input: process.stdin, + crlfDelay: Infinity, + }); + for await (const line of lines) { + if (!line.trim()) continue; + const message = JSON.parse(line); + if (!message || typeof message !== "object" || Array.isArray(message)) { + throw new Error("Invalid Cargo JSON record"); + } + switch (message.reason) { + case "compiler-message": + if (typeof message.message?.rendered === "string") { + process.stderr.write(message.message.rendered); + } + break; + case "compiler-artifact": + if ( + message.target?.name === "render_wasm" && + typeof message.manifest_path === "string" && + path.resolve(message.manifest_path) === manifest + ) { + artifacts.push(message); + } + break; + case "build-script-executed": { + const outDirs = buildScripts.get(message.package_id) || new Set(); + outDirs.add(message.out_dir); + buildScripts.set(message.package_id, outDirs); + break; + } + case "build-finished": + succeeded = message.success === true; + break; + } + } + + if (!succeeded) throw new Error("Cargo build did not finish successfully"); + if (artifacts.length !== 1) { + throw new Error("Expected one root render_wasm compiler-artifact"); + } + const artifact = artifacts[0]; + if ( + typeof artifact.package_id !== "string" || + !artifact.package_id || + !Array.isArray(artifact.features) || + !artifact.features.every((feature) => typeof feature === "string") + ) { + throw new Error("Missing root renderer package_id or actual features"); + } + if ( + !artifact.profile || + typeof artifact.profile !== "object" || + Array.isArray(artifact.profile) || + typeof artifact.profile.opt_level !== "string" || + typeof artifact.profile.debug_assertions !== "boolean" + ) { + throw new Error("Missing or malformed root renderer Cargo profile"); + } + const outDirs = [...(buildScripts.get(artifact.package_id) || [])]; + if (outDirs.length !== 1 || typeof outDirs[0] !== "string" || !outDirs[0]) { + throw new Error( + "Expected one matching root renderer build-script-executed out_dir", + ); + } + const jsFiles = ( + Array.isArray(artifact.filenames) ? artifact.filenames : [] + ).filter( + (file) => + typeof file === "string" && path.basename(file) === "render_wasm.js", + ); + if (jsFiles.length !== 1) + throw new Error("Missing root render_wasm.js artifact"); + + const outDir = path.resolve(outDirs[0]); + const sharedFile = path.join(outDir, "render_wasm_shared.js"); + fs.accessSync(sharedFile, fs.constants.R_OK); + const info = { + package_id: artifact.package_id, + features: artifact.features, + profile: artifact.profile, + out_dir: outDir, + artifact_dir: path.dirname(path.resolve(jsFiles[0])), + shared_file: sharedFile, + }; + fs.mkdirSync(path.dirname(infoFile), { recursive: true }); + const temporary = `${infoFile}.tmp`; + try { + fs.writeFileSync(temporary, JSON.stringify(info, null, 2) + "\n"); + fs.renameSync(temporary, infoFile); + } finally { + fs.rmSync(temporary, { force: true }); + } +} + +main().catch((error) => { + process.stderr.write(`ERROR: ${error.message}\n`); + process.exitCode = 1; + process.stdin.destroy(); +}); diff --git a/render-wasm/scripts/cargo-build.test.cjs b/render-wasm/scripts/cargo-build.test.cjs new file mode 100644 index 0000000000..e757c7cd95 --- /dev/null +++ b/render-wasm/scripts/cargo-build.test.cjs @@ -0,0 +1,349 @@ +const assert = require("node:assert/strict"); +const { spawn, spawnSync } = require("node:child_process"); +const { once } = require("node:events"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { test } = require("node:test"); + +const helper = path.join(__dirname, "cargo-build.cjs"); +const finished = { reason: "build-finished", success: true }; + +function fixture(t) { + const root = fs.mkdtempSync( + path.join(os.tmpdir(), "render-wasm-cargo-build-"), + ); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const manifest = path.join(root, "Cargo.toml"); + const artifactDir = path.join( + root, + "target with spaces", + "wasm32-unknown-emscripten", + "release", + ); + const outDir = path.join(artifactDir, "build", "render-active", "out"); + const staleOutDir = path.join( + artifactDir, + "build", + "000-render-stale", + "out", + ); + for (const dir of [outDir, staleOutDir]) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(manifest, '[package]\nname = "render"\n'); + fs.writeFileSync( + path.join(artifactDir, "render_wasm.js"), + "export default {};\n", + ); + fs.writeFileSync( + path.join(outDir, "render_wasm_shared.js"), + "active enum table\n", + ); + fs.writeFileSync( + path.join(staleOutDir, "render_wasm_shared.js"), + "stale enum table\n", + ); + + const artifact = { + reason: "compiler-artifact", + package_id: `path+file://${root}#render@0.1.0`, + manifest_path: manifest, + target: { name: "render_wasm", kind: ["bin"] }, + features: ["default", "profile", "profile-macros", "profile-raf"], + profile: { + opt_level: "3", + debuginfo: 0, + debug_assertions: false, + overflow_checks: false, + test: false, + }, + filenames: [path.join(artifactDir, "render_wasm.js")], + fresh: false, + }; + const buildScript = { + reason: "build-script-executed", + package_id: artifact.package_id, + out_dir: outDir, + }; + const otherScript = { + ...buildScript, + package_id: + "registry+https://github.com/rust-lang/crates.io-index#render@0.1.0", + out_dir: staleOutDir, + }; + const otherArtifact = { + ...artifact, + package_id: otherScript.package_id, + manifest_path: path.join(root, "dependency", "Cargo.toml"), + features: ["stats"], + profile: { ...artifact.profile, opt_level: "0", debug_assertions: true }, + }; + return { + root, + manifest, + artifactDir, + outDir, + staleOutDir, + artifact, + buildScript, + otherScript, + otherArtifact, + info: path.join(root, "build-info.json"), + }; +} + +function cargoOutput(records) { + return records.map((record) => JSON.stringify(record)).join("\n") + "\n"; +} + +function run(f, records) { + return spawnSync(process.execPath, [helper, f.info, f.manifest], { + cwd: f.root, + input: cargoOutput(records), + encoding: "utf8", + }); +} + +for (const fresh of [false, true]) { + test(`selects the root package's active enum table and actual features (fresh=${fresh})`, (t) => { + const f = fixture(t); + const result = run(f, [ + f.otherScript, + f.otherArtifact, + { ...f.artifact, fresh }, + f.buildScript, + { + ...f.otherScript, + out_dir: path.join(f.root, "another dependency out"), + }, + finished, + ]); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, ""); + const info = JSON.parse(fs.readFileSync(f.info, "utf8")); + assert.deepEqual(info, { + package_id: f.artifact.package_id, + features: f.artifact.features, + profile: f.artifact.profile, + out_dir: f.outDir, + artifact_dir: f.artifactDir, + shared_file: path.join(f.outDir, "render_wasm_shared.js"), + }); + assert.equal( + fs.readFileSync(info.shared_file, "utf8"), + "active enum table\n", + ); + }); +} + +test("records an empty actual feature set", (t) => { + const f = fixture(t); + const result = run(f, [ + f.buildScript, + { ...f.artifact, features: [] }, + finished, + ]); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(fs.readFileSync(f.info, "utf8")).features, []); +}); + +for (const profile of [ + { + opt_level: "0", + debuginfo: 2, + debug_assertions: true, + overflow_checks: true, + test: false, + }, + { + opt_level: "z", + debuginfo: 0, + debug_assertions: false, + overflow_checks: false, + test: false, + }, +]) { + test(`records the actual Cargo profile without changing fields (opt_level=${profile.opt_level})`, (t) => { + const f = fixture(t); + const result = run(f, [ + f.buildScript, + { ...f.artifact, profile }, + finished, + ]); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual( + JSON.parse(fs.readFileSync(f.info, "utf8")).profile, + profile, + ); + }); +} + +test( + "streams rendered compiler messages to stderr before Cargo finishes", + { timeout: 10000 }, + async (t) => { + const f = fixture(t); + const child = spawn(process.execPath, [helper, f.info, f.manifest], { + cwd: f.root, + }); + t.after(() => { + if (child.exitCode === null) child.kill(); + }); + child.stdout.resume(); + const closed = once(child, "close"); + const diagnostic = "warning: renderer diagnostic\n"; + let stderr = ""; + const rendered = new Promise((resolve, reject) => { + child.stderr.on("data", (chunk) => { + stderr += chunk; + if (stderr.includes(diagnostic)) resolve(); + }); + child.on("error", reject); + child.on("close", () => { + if (!stderr.includes(diagnostic)) + reject(new Error(stderr || "Missing diagnostic")); + }); + }); + child.stdin.write( + cargoOutput([ + { reason: "compiler-message", message: { rendered: diagnostic } }, + ]), + ); + await rendered; + assert.equal(child.exitCode, null); + assert.equal(fs.existsSync(f.info), false); + child.stdin.end(cargoOutput([f.buildScript, f.artifact, finished])); + const [code] = await closed; + assert.equal(code, 0, stderr); + assert.equal(stderr, diagnostic); + }, +); + +const rejected = [ + [ + "failed Cargo build", + (f) => [f.buildScript, f.artifact, { ...finished, success: false }], + ], + ["missing build-finished record", (f) => [f.buildScript, f.artifact]], + ["missing root artifact", (f) => [f.otherScript, f.otherArtifact, finished]], + [ + "missing matching build script", + (f) => [f.otherScript, f.artifact, finished], + ], + [ + "missing out_dir", + (f) => [{ ...f.buildScript, out_dir: undefined }, f.artifact, finished], + ], + [ + "missing actual features", + (f) => [f.buildScript, { ...f.artifact, features: undefined }, finished], + ], + [ + "invalid actual features", + (f) => [f.buildScript, { ...f.artifact, features: [42] }, finished], + ], + [ + "missing profile", + (f) => [f.buildScript, { ...f.artifact, profile: undefined }, finished], + ], + [ + "null profile", + (f) => [f.buildScript, { ...f.artifact, profile: null }, finished], + ], + [ + "non-object profile", + (f) => [f.buildScript, { ...f.artifact, profile: "release" }, finished], + ], + [ + "array profile", + (f) => [f.buildScript, { ...f.artifact, profile: [] }, finished], + ], + [ + "missing profile opt_level", + (f) => [ + f.buildScript, + { ...f.artifact, profile: { debug_assertions: false } }, + finished, + ], + ], + [ + "invalid profile opt_level", + (f) => [ + f.buildScript, + { ...f.artifact, profile: { opt_level: 3, debug_assertions: false } }, + finished, + ], + ], + [ + "missing profile debug_assertions", + (f) => [ + f.buildScript, + { ...f.artifact, profile: { opt_level: "3" } }, + finished, + ], + ], + [ + "invalid profile debug_assertions", + (f) => [ + f.buildScript, + { ...f.artifact, profile: { opt_level: "3", debug_assertions: "false" } }, + finished, + ], + ], + [ + "missing JS artifact", + (f) => [f.buildScript, { ...f.artifact, filenames: [] }, finished], + ], + [ + "multiple root artifacts", + (f) => [ + f.buildScript, + f.artifact, + { ...f.artifact, features: [] }, + finished, + ], + ], + [ + "ambiguous root out_dir", + (f) => [ + f.buildScript, + { ...f.buildScript, out_dir: f.staleOutDir }, + f.artifact, + finished, + ], + ], +]; + +for (const [name, records] of rejected) { + test(`rejects ${name} and removes stale build facts`, (t) => { + const f = fixture(t); + fs.writeFileSync(f.info, '{"features":["stale"]}\n'); + const result = run(f, records(f)); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /ERROR:/); + assert.equal(fs.existsSync(f.info), false); + }); +} + +test("rejects a missing active enum table even when a stale table exists", (t) => { + const f = fixture(t); + fs.unlinkSync(path.join(f.outDir, "render_wasm_shared.js")); + const result = run(f, [f.otherScript, f.buildScript, f.artifact, finished]); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /render_wasm_shared\.js/); + assert.equal(fs.existsSync(f.info), false); +}); + +test("rejects malformed Cargo JSON and removes stale build facts", (t) => { + const f = fixture(t); + fs.writeFileSync(f.info, '{"features":["stale"]}\n'); + const result = spawnSync(process.execPath, [helper, f.info, f.manifest], { + cwd: f.root, + input: "not Cargo JSON\n", + encoding: "utf8", + }); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /ERROR:/); + assert.equal(fs.existsSync(f.info), false); +}); diff --git a/render-wasm/test b/render-wasm/test index 5ed665a7d6..3f680ac09a 100755 --- a/render-wasm/test +++ b/render-wasm/test @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -x +set -ex case "$(uname -m)" in aarch64|arm64) _ARCH="aarch64" ;; @@ -14,6 +14,8 @@ pushd "$_SCRIPT_DIR" > /dev/null . ./_build_env frontend +node --test scripts/cargo-build.test.cjs || exit $? + setup cargo test --bin render_wasm -- --show-output diff --git a/render-wasm/watch b/render-wasm/watch index f9ad114bb8..c1177186e2 100755 --- a/render-wasm/watch +++ b/render-wasm/watch @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Usage: ./watch [frontend|export] +# Usage: ./watch [frontend|export] [extra cargo params...] set -e @@ -9,19 +9,26 @@ pushd "$_SCRIPT_DIR" > /dev/null _RENDER_TARGET=${1:-${RENDER_TARGET:-frontend}} export RENDER_TARGET="$_RENDER_TARGET" -. ./_build_env "$_RENDER_TARGET" "${@:2}" +_BUILD_COMMAND=("$_SCRIPT_DIR/build" "$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 +"${_BUILD_COMMAND[@]}" + +# cargo watch runs shell commands; single-quote each original argument for sh. +_WATCH_BUILD_COMMAND="" +for _arg in "${_BUILD_COMMAND[@]}"; do + _arg=${_arg//\'/\'\\\'\'} + _WATCH_BUILD_COMMAND+=" '$_arg'" +done set -x cargo watch \ --why \ + --postpone \ -i "_tmp*" \ - -x "build $CARGO_PARAMS" \ - -s "\"$_SCRIPT_DIR/build\" \"$RENDER_TARGET\" $CARGO_PARAMS" \ + -s "$_WATCH_BUILD_COMMAND" \ -s "echo 'DONE\n'"; popd > /dev/null