diff --git a/npm/build.sh b/npm/build.sh index 281a5758..f730e103 100755 --- a/npm/build.sh +++ b/npm/build.sh @@ -202,10 +202,10 @@ esac # --- optional test ----------------------------------------------------------- # Runs the same smoke test that CI runs (see .github/workflows/main-wasm.yml). +# Run in a subshell so the cd does not leak into the --pack step below, which +# relies on the script's working directory being unchanged. if [ $OPT_TEST -eq 1 ]; then - cd "$SCRIPT_DIR" - npm run test - npm run test:tcl + ( cd "$SCRIPT_DIR" && npm run test && npm run test:tcl ) fi # --- optional pack ----------------------------------------------------------- diff --git a/npm/examples/all-tcl.js b/npm/examples/all-tcl.js index 20ec5c12..7fd14ff2 100644 --- a/npm/examples/all-tcl.js +++ b/npm/examples/all-tcl.js @@ -9,6 +9,7 @@ import { run as runGds } from './gds-tcl.js'; import { run as runDrc } from './drc-tcl.js'; import { run as runCif } from './cif-tcl.js'; import { run as runPcell } from './pcell.js'; +import { reportError } from './helpers-tcl.js'; const PAD = 9; @@ -20,6 +21,9 @@ async function test(name, fn) { return true; } catch (e) { console.log(`FAIL ${e.message ?? e}`); + // Full stack to stderr so CI shows the wasm-function offsets, while the + // one-line summary above stays readable. Other tests still run. + reportError(e); return false; } } diff --git a/npm/examples/all.js b/npm/examples/all.js index 8bc4985d..44699150 100644 --- a/npm/examples/all.js +++ b/npm/examples/all.js @@ -5,6 +5,7 @@ import { run as runExtract } from './extract.js'; import { run as runGds } from './gds.js'; import { run as runDrc } from './drc.js'; import { run as runCif } from './cif.js'; +import { reportError } from './helpers.js'; const PAD = 9; @@ -16,6 +17,9 @@ async function test(name, fn) { return true; } catch (e) { console.log(`FAIL ${e.message ?? e}`); + // Full stack to stderr so CI shows the wasm-function offsets, while the + // one-line summary above stays readable. Other tests still run. + reportError(e); return false; } } diff --git a/npm/examples/cif-tcl.js b/npm/examples/cif-tcl.js index 3a6dbc62..212addbe 100644 --- a/npm/examples/cif-tcl.js +++ b/npm/examples/cif-tcl.js @@ -1,6 +1,6 @@ // cif-tcl.js — CIF export via the TCL variant. import { createMagic, vfsRead, loadCell, loadScript, - DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers-tcl.js'; + DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT, reportError } from './helpers-tcl.js'; import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; @@ -22,7 +22,7 @@ export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDi } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const { outPath, bytes } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const { outPath, bytes } = await run().catch(e => { reportError(e); process.exit(1); }); console.log(`\ncif: ${outPath} (${bytes} bytes)`); console.log('Done.'); } diff --git a/npm/examples/cif.js b/npm/examples/cif.js index b24653b9..c41cb449 100644 --- a/npm/examples/cif.js +++ b/npm/examples/cif.js @@ -2,7 +2,7 @@ // // Usage: node examples/cif.js [magFile [tech [outputDir]]] import { createMagic, vfsRead, loadCell, loadScript, - DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers.js'; + DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT, reportError } from './helpers.js'; import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; @@ -24,7 +24,7 @@ export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDi } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const { outPath, bytes } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const { outPath, bytes } = await run().catch(e => { reportError(e); process.exit(1); }); console.log(`\ncif: ${outPath} (${bytes} bytes)`); console.log('Done.'); } diff --git a/npm/examples/drc-tcl.js b/npm/examples/drc-tcl.js index 2651af32..20258bd5 100644 --- a/npm/examples/drc-tcl.js +++ b/npm/examples/drc-tcl.js @@ -1,6 +1,6 @@ // drc-tcl.js — DRC check via the TCL variant. import { createMagic, loadCell, loadScript, - DEFAULT_TECH, DEFAULT_MAG } from './helpers-tcl.js'; + DEFAULT_TECH, DEFAULT_MAG, reportError } from './helpers-tcl.js'; import { fileURLToPath } from 'node:url'; export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH } = {}) { @@ -22,7 +22,7 @@ export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH } = {}) { } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const { violations } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const { violations } = await run().catch(e => { reportError(e); process.exit(1); }); console.log(`\nDRC violations: ${violations ?? '(count not found in output)'}`); console.log('Done.'); } diff --git a/npm/examples/drc.js b/npm/examples/drc.js index 204f461d..52480fe2 100644 --- a/npm/examples/drc.js +++ b/npm/examples/drc.js @@ -2,7 +2,7 @@ // // Usage: node examples/drc.js [magFile [tech]] import { createMagic, loadCell, loadScript, - DEFAULT_TECH, DEFAULT_MAG } from './helpers.js'; + DEFAULT_TECH, DEFAULT_MAG, reportError } from './helpers.js'; import { fileURLToPath } from 'node:url'; export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH } = {}) { @@ -25,7 +25,7 @@ export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH } = {}) { } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const { violations } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const { violations } = await run().catch(e => { reportError(e); process.exit(1); }); console.log(`\nDRC violations: ${violations ?? '(count not found in output)'}`); console.log('Done.'); } diff --git a/npm/examples/extract-tcl.js b/npm/examples/extract-tcl.js index 193fb2ce..c307bdaf 100644 --- a/npm/examples/extract-tcl.js +++ b/npm/examples/extract-tcl.js @@ -1,6 +1,6 @@ // extract-tcl.js — RC extraction via the TCL variant. import { createMagic, vfsWrite, vfsRead, loadCell, loadScript, - DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers-tcl.js'; + DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT, reportError } from './helpers-tcl.js'; import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; @@ -32,7 +32,7 @@ export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDi } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const { ext, spice } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const { ext, spice } = await run().catch(e => { reportError(e); process.exit(1); }); console.log(`\next: ${ext}`); if (spice) console.log(`spice: ${spice}`); console.log('Done.'); diff --git a/npm/examples/extract.js b/npm/examples/extract.js index f2af0adf..2a6e18e2 100644 --- a/npm/examples/extract.js +++ b/npm/examples/extract.js @@ -2,7 +2,7 @@ // // Usage: node examples/extract.js [magFile [tech [outputDir]]] import { createMagic, vfsWrite, vfsRead, loadCell, loadScript, - DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers.js'; + DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT, reportError } from './helpers.js'; import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; @@ -34,7 +34,7 @@ export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDi } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const { ext, spice } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const { ext, spice } = await run().catch(e => { reportError(e); process.exit(1); }); console.log(`\next: ${ext}`); if (spice) console.log(`spice: ${spice}`); console.log('Done.'); diff --git a/npm/examples/gds-tcl.js b/npm/examples/gds-tcl.js index 98bec535..5e219624 100644 --- a/npm/examples/gds-tcl.js +++ b/npm/examples/gds-tcl.js @@ -1,6 +1,6 @@ // gds-tcl.js — GDS export via the TCL variant. import { createMagic, vfsRead, loadCell, loadScript, - DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers-tcl.js'; + DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT, reportError } from './helpers-tcl.js'; import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; @@ -22,7 +22,7 @@ export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDi } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const { outPath, bytes } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const { outPath, bytes } = await run().catch(e => { reportError(e); process.exit(1); }); console.log(`\ngds: ${outPath} (${bytes} bytes)`); console.log('Done.'); } diff --git a/npm/examples/gds.js b/npm/examples/gds.js index c07cba76..8f4ae974 100644 --- a/npm/examples/gds.js +++ b/npm/examples/gds.js @@ -2,7 +2,7 @@ // // Usage: node examples/gds.js [magFile [tech [outputDir]]] import { createMagic, vfsRead, loadCell, loadScript, - DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers.js'; + DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT, reportError } from './helpers.js'; import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; @@ -24,7 +24,7 @@ export async function run({ magFile = DEFAULT_MAG, tech = DEFAULT_TECH, outputDi } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const { outPath, bytes } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const { outPath, bytes } = await run().catch(e => { reportError(e); process.exit(1); }); console.log(`\ngds: ${outPath} (${bytes} bytes)`); console.log('Done.'); } diff --git a/npm/examples/helpers-tcl.js b/npm/examples/helpers-tcl.js index 35c12398..b4f25e4c 100644 --- a/npm/examples/helpers-tcl.js +++ b/npm/examples/helpers-tcl.js @@ -43,7 +43,14 @@ export class MagicWasm { const path = '/tmp/_magic_script.tcl'; this.FS.mkdirTree('/tmp'); this.FS.writeFile(path, text); - this._sourceFile(path); + try { + this._sourceFile(path); + } catch (e) { + // A WASM trap during Tcl evaluation otherwise gives no hint of the + // source; flag it before the error propagates. + console.error('[magic-tcl] script evaluation failed'); + throw e; + } } // Evaluate a Tcl expression directly (needed for proc definitions in PCell). runTcl(text) { @@ -74,3 +81,11 @@ export function loadScript(name, tech, cell) { .replaceAll('__TECH__', tech) .replaceAll('__CELL__', cell); } + +// Print a failure with enough detail to be actionable in CI. WASM aborts +// surface as a RuntimeError whose .message is terse while .stack carries the +// wasm-function offsets that emsymbolizer maps back to C source — so always +// prefer the stack. +export function reportError(e) { + console.error(e?.stack ?? String(e?.message ?? e)); +} diff --git a/npm/examples/helpers.js b/npm/examples/helpers.js index 9092fb6c..1f0cadfb 100644 --- a/npm/examples/helpers.js +++ b/npm/examples/helpers.js @@ -38,11 +38,28 @@ export class MagicWasm { runScript(text) { for (const line of text.split('\n')) { const l = line.trim(); - if (l && !l.startsWith('#')) this._run(l); + if (l && !l.startsWith('#')) { + try { + this._run(l); + } catch (e) { + // Name the command that aborted before the error propagates — a + // WASM trap otherwise gives no hint which step failed. + console.error(`[magic] command failed: ${l}`); + throw e; + } + } } } } +// Print a failure with enough detail to be actionable in CI. WASM aborts +// surface as a RuntimeError whose .message is terse ("memory access out of +// bounds") while .stack carries the wasm-function offsets that emsymbolizer +// maps back to C source — so always prefer the stack. +export function reportError(e) { + console.error(e?.stack ?? String(e?.message ?? e)); +} + // Creates a fresh Magic WASM instance and calls init(). // onPrint / onPrintErr default to console.log/error with a [magic] prefix. // All output lines are also collected into the returned `lines` array. diff --git a/npm/examples/pcell.js b/npm/examples/pcell.js index b31066fd..2d7b899d 100644 --- a/npm/examples/pcell.js +++ b/npm/examples/pcell.js @@ -4,7 +4,7 @@ // and verifies that both GDS outputs are non-empty. // // Usage: node examples/pcell.js -import { createMagic, vfsRead, loadScript, DEFAULT_TECH, DEFAULT_OUT } from './helpers-tcl.js'; +import { createMagic, vfsRead, loadScript, DEFAULT_TECH, DEFAULT_OUT, reportError } from './helpers-tcl.js'; import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; @@ -32,7 +32,7 @@ export async function run({ tech = DEFAULT_TECH, outputDir = DEFAULT_OUT } = {}) } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const results = await run().catch(e => { console.error(e.message ?? e); process.exit(1); }); + const results = await run().catch(e => { reportError(e); process.exit(1); }); for (const [name, { outPath, bytes }] of Object.entries(results)) console.log(` ${name}.gds: ${outPath} (${bytes} bytes)`); console.log('Done.');