Add npm package and CI workflows

The user-facing layer of the WASM port: a publishable npm package
plus the GitHub Actions that build and ship it.

* npm/package.json — publishes as `magic-vlsi-wasm`, ESM-only, HPND
  licensed, version tracks Magic's own VERSION file (8.3.637).
  Whitelists the published files and exposes index.js + index.d.ts.

* npm/index.js, npm/index.d.ts — thin JS/TS wrapper around the four
  WASM exports. createMagic(opts) returns { init, runCommand,
  sourceFile, update, FS } so consumers can write into the
  Emscripten virtual filesystem and dispatch Magic commands from
  Node.js, browsers or Web Workers.

* npm/build.sh — end-to-end build: locates emsdk (via PATH or
  EMSDK_DIR), runs distclean+configure+make in the right order
  (techs before mains so embed-files are present), copies
  magic.js / magic.wasm into npm/. Optional --release, --test,
  --pack flags. Preserves configure's exec bits across invocations.

* npm/pack.sh — produces a reproducible npm tarball by touching
  every file to the build time and exporting SOURCE_DATE_EPOCH so
  pacote does not rewrite mtimes to its 1985 fallback.

* npm/examples/ — runnable smoke tests for the four common
  workflows (extract, gds, drc, cif), driven by examples/all.js.
  Each example is self-contained and uses the bundled siliwiz
  technology. helpers.js encapsulates the boilerplate.

* npm/LICENSE, npm/README.md — license text and consumer-facing
  docs (install, quick-start, API, examples, build-from-source,
  license, third-party content notice).

* .github/workflows/main.yml — adds a `simple_build_wasm` job that
  installs a pinned emsdk (3.1.56), builds the WASM module, runs
  the example test suite and uploads the npm tarball as an
  artifact. Pinned for reproducibility against the post-build.sh
  patches; switchable to "latest" by commenting two lines.

* .github/workflows/main-aarch64.yml — drops the now-redundant
  WASM ARM job. WASM is architecture-independent.

* .github/workflows/npm-publish.yml — new workflow. Publishes to
  npm on `v*` tag pushes (manual `workflow_dispatch` supported as
  a dry-run). Uses the same pinned emsdk and pack.sh.

Also sets FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 in both workflows to
silence the Node.js 20 deprecation warnings until
actions/upload-artifact@v6 ships a Node-24 release.
This commit is contained in:
Intubun 2026-05-04 13:32:51 +02:00 committed by R. Timothy Edwards
parent 02087ea3f8
commit 8560027569
24 changed files with 2392 additions and 55 deletions

View File

@ -1,14 +1,15 @@
# This is a basic workflow to help you get started with Actions
# CI for native ARM64 Linux build.
# WASM is not built here — WebAssembly bytecode is architecture-independent,
# so the single x86-64 WASM artifact produced by the CI workflow is identical
# to what an ARM build would produce. See .github/workflows/main.yml.
name: CI-aarch64
# Controls when the workflow will run
on:
push:
pull_request:
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
simple_build_linux_arm:
runs-on: ubuntu-24.04-arm
@ -23,39 +24,3 @@ jobs:
./configure
make database/database.h
make -j$(nproc)
simple_build_wasm_arm:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- name: Get Dependencies
run: |
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
- name: Emscripten Diagnostic
run: |
source ./emsdk/emsdk_env.sh
echo "===== gcc -dM -E - ====="
echo | gcc -dM -E - | sort
echo "===== g++ -dM -E - ====="
echo | g++ -dM -E - | sort
echo "===== emcc -dM -E - ====="
echo | emcc -dM -E - | sort
echo "===== em++ -dM -E - ====="
echo | em++ -dM -E - | sort
- name: Build
run: |
source ./emsdk/emsdk_env.sh
# The --without and --disable in these build options is due to no WASM library being available for that feature
CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1 -g" emconfigure ./configure --without-cairo --without-opengl --without-x --without-tk --without-tcl --disable-readline --disable-compression --target=asmjs-unknown-emscripten
echo "===== defs.mak ====="
cat defs.mak
echo "===== defs.mak ====="
emmake make
- name: archive wasm bundle
uses: actions/upload-artifact@v4
with:
name: magic-wasm-bundle-arm
path: |
${{ github.workspace }}/magic/magic.wasm

View File

@ -1,19 +1,21 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the workflow will run
on:
push:
pull_request:
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
# actions/upload-artifact@v5 still runs on Node.js 20. Force Node 24 to
# silence the deprecation warning until upload-artifact ships a Node-24
# release. Drop this once upgraded.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
simple_build_linux:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get Dependencies
run: |
sudo apt-get install -y tcl-dev tk-dev libcairo-dev
@ -24,14 +26,24 @@ jobs:
make -j$(nproc)
simple_build_wasm:
runs-on: ubuntu-22.04
env:
# --- emsdk version switch ---------------------------------------------
# Pinned: post-build.sh patches depend on the exact text output of this
# Emscripten version. Bump intentionally and re-verify the patches.
# To test against the latest emsdk, comment the pinned line and
# uncomment the "latest" line below.
EMSDK_VERSION: '3.1.56'
# EMSDK_VERSION: 'latest'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get Dependencies
run: |
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
./emsdk install "$EMSDK_VERSION"
./emsdk activate "$EMSDK_VERSION"
# Dump native + emscripten preprocessor defines. Useful for diagnosing
# WASM-build differences after an emsdk bump.
- name: Emscripten Diagnostic
run: |
source ./emsdk/emsdk_env.sh
@ -46,15 +58,36 @@ jobs:
- name: Build
run: |
source ./emsdk/emsdk_env.sh
# The --without and --disable in these build options is due to no WASM library being available for that feature
CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1 -g" emconfigure ./configure --without-cairo --without-opengl --without-x --without-tk --without-tcl --disable-readline --disable-compression --target=asmjs-unknown-emscripten
# --without/--disable flags: no WASM library available for these features
CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1 -g" emconfigure ./configure \
--without-cairo --without-opengl --without-x --without-tk --without-tcl \
--disable-readline --disable-compression \
--host=asmjs-unknown-emscripten \
--target=asmjs-unknown-emscripten
# Append WASM linker flags and activate the WASM link target
cat toolchains/emscripten/defs.mak >> defs.mak
# Echo the merged defs.mak so CI logs show the exact build config
echo "===== defs.mak ====="
cat defs.mak
echo "===== defs.mak ====="
emmake make
- name: archive wasm bundle
uses: actions/upload-artifact@v4
# Build in order: techs must exist before mains (--embed-file embeds them)
emmake make depend
emmake make -j$(nproc) modules libs
emmake make techs
emmake make mains
- name: Set up Node.js
uses: actions/setup-node@v5
with:
name: magic-wasm-bundle
path: |
${{ github.workspace }}/magic/magic.wasm
node-version: '22'
- name: Run example tests
run: |
cp magic/magic.js npm/
cp magic/magic.wasm npm/
cd npm && npm run test
- name: Pack npm package
run: ./npm/pack.sh
- name: Upload npm package
uses: actions/upload-artifact@v5
with:
name: magic-vlsi-wasm-npm
path: ${{ github.workspace }}/npm/*.tgz

95
.github/workflows/npm-publish.yml vendored Normal file
View File

@ -0,0 +1,95 @@
name: Publish npm package
# Publishes magic-vlsi-wasm to npm.
# Triggered automatically on version tags (v*); can also be run manually.
# Requires an NPM_TOKEN repository secret for publishing.
#
# WASM is architecture-independent — built once on x86-64, usable everywhere.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run: pack only, do not publish'
type: boolean
default: true
# actions/upload-artifact@v5 still runs on Node.js 20. Force Node 24 to
# silence the deprecation warning until upload-artifact ships a Node-24
# release. Drop this once upgraded.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build-and-publish:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Install emsdk
env:
# --- emsdk version switch -----------------------------------------
# Pinned: must match .github/workflows/main.yml. See post-build.sh.
# To test against the latest emsdk, comment the pinned line and
# uncomment the "latest" line below.
EMSDK_VERSION: '3.1.56'
# EMSDK_VERSION: 'latest'
run: |
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install "$EMSDK_VERSION"
./emsdk activate "$EMSDK_VERSION"
- name: Build WASM
run: |
source ./emsdk/emsdk_env.sh
# --without/--disable flags: no WASM library available for these features
CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1" emconfigure ./configure \
--without-cairo --without-opengl --without-x --without-tk --without-tcl \
--disable-readline --disable-compression \
--host=asmjs-unknown-emscripten \
--target=asmjs-unknown-emscripten
# Append WASM linker flags and activate the WASM link target
cat toolchains/emscripten/defs.mak >> defs.mak
# Build in order: techs must exist before mains (--embed-file embeds them)
emmake make depend
emmake make -j$(nproc) modules libs
emmake make techs
emmake make mains
- name: Copy WASM artifacts into npm/
run: |
cp magic/magic.js npm/
cp magic/magic.wasm npm/
- name: Set package version from tag
if: startsWith(github.ref, 'refs/tags/v')
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
cd npm
npm version "$VERSION" --no-git-tag-version
- name: Pack
run: ./npm/pack.sh
- name: Upload tarball as artifact
uses: actions/upload-artifact@v5
with:
name: magic-wasm-npm-package
path: npm/*.tgz
- name: Publish to npm
if: startsWith(github.ref, 'refs/tags/v') && github.event.inputs.dry_run != 'true'
run: cd npm && npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

6
npm/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
magic.js
magic.wasm
*.tgz
node_modules/
package-lock.json
examples/output/

10
npm/LICENSE Normal file
View File

@ -0,0 +1,10 @@
Copyright (C) 1985, 1990 Regents of the University of California.
Permission to use, copy, modify, and distribute this
software and its documentation for any purpose and without
fee is hereby granted, provided that the above copyright
notice appear in all copies. The University of California
makes no representations about the suitability of this
software for any purpose. It is provided "as is" without
express or implied warranty. Export of this software outside
of the United States of America may require an export license.

127
npm/README.md Normal file
View File

@ -0,0 +1,127 @@
# magic-vlsi-wasm
[Magic VLSI](http://opencircuitdesign.com/magic/) layout tool, compiled to
WebAssembly as a headless library. Runs in Node.js, browsers, and Web Workers
— no X11, no Tk, no native dependencies.
Use it to programmatically read and write `.mag`, `.gds`, `.cif`, `.ext`, and
SPICE netlists; run DRC; extract parasitics — anywhere JavaScript runs.
## Install
```bash
npm install magic-vlsi-wasm
```
Requires Node.js 18 or newer.
## Quick start
```js
import createMagic from 'magic-vlsi-wasm';
const { runCommand, FS } = await createMagic();
// Write a layout into Magic's virtual filesystem
FS.writeFile('/work/inv.mag', layoutBytes);
// Run Magic commands
runCommand('tech load minimum');
runCommand('load /work/inv');
runCommand('gds write /work/inv');
// Read the result back out
const gdsBytes = FS.readFile('/work/inv.gds');
```
The `scmos` technology family (`scmos`, `minimum`, `nmos`, ...) is embedded in
the WASM binary and available out of the box. Custom tech files can be written
into the VFS at `/magic/sys/current/<name>.tech`.
## API
```ts
createMagic(options?): Promise<MagicInstance>
```
`options` is forwarded to the underlying Emscripten module. Useful keys:
| Key | Default | Purpose |
|--------------|------------------|---------|
| `wasmBinary` | fetched lazily | Pre-fetched ArrayBuffer of `magic.wasm` (skips a network round-trip in browsers) |
| `print` | `console.log` | Callback for each stdout line |
| `printErr` | `console.error` | Callback for each stderr line |
The returned `MagicInstance` exposes:
| Method | Description |
|---------------------------|-------------|
| `runCommand(cmd: string)` | Dispatch a single Magic command. Returns 0 on success. |
| `sourceFile(path: string)` | Execute a script from the virtual filesystem. |
| `init()` | Force initialization. Idempotent — `runCommand` and `sourceFile` call it for you. |
| `update()` | Drive a display-update cycle. No-op in this headless build. |
| `FS` | Emscripten virtual filesystem. See the [Emscripten docs](https://emscripten.org/docs/api_reference/Filesystem-API.html). |
Full TypeScript types ship in [`index.d.ts`](index.d.ts).
## Examples
The package ships runnable examples for the most common workflows. After
installing, run one directly:
```bash
node node_modules/magic-vlsi-wasm/examples/extract.js
node node_modules/magic-vlsi-wasm/examples/gds.js
node node_modules/magic-vlsi-wasm/examples/drc.js
node node_modules/magic-vlsi-wasm/examples/cif.js
```
Or, when developing inside this repo:
```bash
npm test # full suite (extract, gds, drc, cif)
npm run test:gds # GDS write only
npm run test:drc # DRC check only
npm run test:cif # CIF write only
```
Each example is self-contained and reads `examples/siliwiz.{mag,tech}` by
default. See [`examples/`](examples/) for the source.
## Build from source
If you want to rebuild the WASM module yourself, see
[`toolchains/emscripten/README.md`](../toolchains/emscripten/README.md).
The short version:
```bash
bash npm/build.sh # debug build, copies magic.js + magic.wasm into npm/
bash npm/build.sh --release # optimized
bash npm/build.sh --test # build + run tests
bash npm/build.sh --pack # build + produce magic-vlsi-wasm-<version>.tgz
```
You will need an activated [emsdk](https://emscripten.org/docs/getting_started/downloads.html)
checkout (Magic pins emsdk `3.1.56` — see the comment in `npm/build.sh`).
## Limitations
- Headless only. There is no display driver, so commands that draw to a
window (`view`, `findbox`, interactive macros) are no-ops.
- WASM memory starts at 32 MB and grows as needed. Very large GDS imports
may need `INITIAL_MEMORY` bumped (rebuild required).
- Single-threaded. WASM modules are not thread-safe — create one instance
per worker.
## License
[HPND](LICENSE) — Copyright (C) 1985, 1990 Regents of the University of California.
### Bundled test technology
The example layout (`examples/siliwiz.mag`) and technology file
(`examples/siliwiz.tech`) are derived from the
[SiliWiz](https://github.com/wokwi/siliwiz) educational silicon design
tool. They are bundled here only as a runnable smoke test for the WASM
build. The technology file is © R. Timothy Edwards, Open Circuit Design,
2023, marked by the author as containing no proprietary information.

132
npm/build.sh Executable file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env bash
# Build Magic WASM and copy artifacts into this npm/ directory.
#
# Usage:
# npm/build.sh [--release] [--test] [--pack]
#
# --release Omit debug symbols (-g).
# --test Run `npm run test` after copying artifacts.
# --pack Run `npm pack` after copying artifacts (and tests, if given).
#
# Requirements (must be on PATH or set via env vars before running):
# emcc / emmake / emconfigure — Emscripten compiler tools
# make, gcc — standard build tools
# node, npm — only required for --test / --pack
#
# Environment:
# EMSDK_DIR Path to an activated emsdk checkout.
# If set, emsdk_env.sh is sourced from there.
# If unset, emcc must already be on PATH (e.g. sourced externally).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
OPT_RELEASE=0
OPT_TEST=0
OPT_PACK=0
for arg in "$@"; do
case "$arg" in
--release) OPT_RELEASE=1 ;;
--test) OPT_TEST=1 ;;
--pack) OPT_PACK=1 ;;
*) echo "Unknown option: $arg" >&2; exit 1 ;;
esac
done
# --- locate emscripten -------------------------------------------------------
if [ -n "${EMSDK_DIR:-}" ]; then
if [ ! -f "$EMSDK_DIR/emsdk_env.sh" ]; then
echo "Error: EMSDK_DIR is set to '$EMSDK_DIR' but emsdk_env.sh was not found there." >&2
exit 1
fi
# shellcheck source=/dev/null
source "$EMSDK_DIR/emsdk_env.sh"
else
if ! command -v emcc &>/dev/null; then
echo "Error: emcc not found on PATH and EMSDK_DIR is not set." >&2
echo " Either source emsdk_env.sh before running this script," >&2
echo " or set EMSDK_DIR to your emsdk checkout directory." >&2
exit 1
fi
fi
echo "Using emcc: $(command -v emcc)"
emcc --version | head -1
# --- portability helpers -----------------------------------------------------
# CPU count: Linux has nproc, macOS has sysctl, fall back to getconf.
ncpu() {
if command -v nproc &>/dev/null; then
nproc
elif [ "$(uname)" = "Darwin" ]; then
sysctl -n hw.ncpu
else
getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1
fi
}
# Portable in-place sed (BSD sed on macOS disagrees with GNU on -i).
# Uses redirect-back instead of mv so the file's mode bits are preserved
# (configure must stay executable across build.sh invocations).
sed_strip_cr() {
local file=$1 tmp
tmp=$(mktemp)
sed 's/\r//' "$file" > "$tmp" && cat "$tmp" > "$file" && rm "$tmp"
}
# --- clean -------------------------------------------------------------------
cd "$REPO_ROOT"
# Only distclean if there's something to clean. A stale `|| true` here would
# hide real failures (e.g. broken toolchain) on a fresh checkout.
if [ -f defs.mak ]; then
emmake make distclean || true
fi
rm -f defs.mak database/database.h
# --- configure ---------------------------------------------------------------
# Strip Windows CRLF line endings (no-op on Linux-native files).
sed_strip_cr configure
find scripts/ -type f -print0 | while IFS= read -r -d '' f; do sed_strip_cr "$f"; done
if [ $OPT_RELEASE -eq 1 ]; then
EXTRA_CFLAGS=" -O2"
else
EXTRA_CFLAGS=" -g"
fi
CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1${EXTRA_CFLAGS}" \
emconfigure ./configure \
--without-cairo --without-opengl --without-x --without-tk --without-tcl \
--disable-readline --disable-compression \
--host=asmjs-unknown-emscripten \
--target=asmjs-unknown-emscripten
cat toolchains/emscripten/defs.mak >> defs.mak
# --- build -------------------------------------------------------------------
emmake make depend
emmake make -j"$(ncpu)" modules libs
emmake make techs
emmake make mains
# --- copy artifacts ----------------------------------------------------------
cp magic/magic.js "$SCRIPT_DIR/"
cp magic/magic.wasm "$SCRIPT_DIR/"
echo "Copied magic.js and magic.wasm to npm/"
# --- optional test -----------------------------------------------------------
# Runs the same smoke test that CI runs (see .github/workflows/main.yml).
if [ $OPT_TEST -eq 1 ]; then
cd "$SCRIPT_DIR"
npm run test
fi
# --- optional pack -----------------------------------------------------------
if [ $OPT_PACK -eq 1 ]; then
"$SCRIPT_DIR/pack.sh"
echo "npm package tarball created in npm/"
fi

50
npm/examples/all.js Normal file
View File

@ -0,0 +1,50 @@
// all.js — Run all Magic WASM example tests and print a summary.
//
// Usage: node examples/all.js
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';
const PAD = 9;
async function test(name, fn) {
process.stdout.write(` ${name.padEnd(PAD)} `);
try {
const result = await fn();
console.log(`PASS ${formatResult(name, result)}`);
return true;
} catch (e) {
console.log(`FAIL ${e.message ?? e}`);
return false;
}
}
function formatResult(name, r) {
if (!r) return '';
switch (name) {
case 'extract': return [r.ext, r.spice].filter(Boolean).map(p => p.split(/[\\/]/).pop()).join(', ');
case 'gds': return `${r.outPath.split(/[\\/]/).pop()} (${r.bytes} B)`;
case 'cif': return `${r.outPath.split(/[\\/]/).pop()} (${r.bytes} B)`;
case 'drc': return r.violations != null ? `${r.violations} violation${r.violations !== 1 ? 's' : ''}` : '';
default: return '';
}
}
console.log('\nMagic WASM — test suite\n');
const suite = [
['extract', runExtract],
['gds', runGds],
['drc', runDrc],
['cif', runCif],
];
const passed = [];
for (const [name, fn] of suite) {
passed.push(await test(name, fn));
}
const ok = passed.filter(Boolean).length;
console.log(`\n${ok}/${suite.length} passed`);
process.exit(ok === suite.length ? 0 : 1);

30
npm/examples/cif.js Normal file
View File

@ -0,0 +1,30 @@
// cif.js — Export layout to CIF (Caltech Intermediate Form).
//
// Usage: node examples/cif.js [magFile [techFile [outputDir]]]
import { createMagic, vfsRead, loadCell, loadScript,
DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers.js';
import { writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
export async function run({ magFile = DEFAULT_MAG, techFile = DEFAULT_TECH, outputDir = DEFAULT_OUT } = {}) {
const { magic } = await createMagic();
const { FS } = magic;
const { tech, cell } = loadCell(FS, techFile, magFile);
magic.runScript(loadScript('cif.tcl', tech, cell));
mkdirSync(outputDir, { recursive: true });
const data = vfsRead(FS, `/work/${cell}.cif`);
if (!data) throw new Error(`CIF export failed: /work/${cell}.cif not created`);
const outPath = resolve(outputDir, `${cell}.cif`);
writeFileSync(outPath, data);
return { outPath, bytes: data.length };
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const { outPath, bytes } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
console.log(`\ncif: ${outPath} (${bytes} bytes)`);
console.log('Done.');
}

4
npm/examples/cif.tcl Normal file
View File

@ -0,0 +1,4 @@
# CIF export
tech load __TECH__
load /work/__CELL__
cif write /work/__CELL__

31
npm/examples/drc.js Normal file
View File

@ -0,0 +1,31 @@
// drc.js — Run design rule checking and report violations.
//
// Usage: node examples/drc.js [magFile [techFile]]
import { createMagic, loadCell, loadScript,
DEFAULT_TECH, DEFAULT_MAG } from './helpers.js';
import { fileURLToPath } from 'node:url';
export async function run({ magFile = DEFAULT_MAG, techFile = DEFAULT_TECH } = {}) {
const output = [];
const { magic } = await createMagic({
onPrint: msg => { output.push(msg); console.log('[magic]', msg); },
onPrintErr: msg => { output.push(msg); console.error('[magic]', msg); },
});
const { FS } = magic;
const { tech, cell } = loadCell(FS, techFile, magFile);
magic.runScript(loadScript('drc.tcl', tech, cell));
// Magic prints "Total DRC errors found: N" at the end of drc listall.
const summary = output.find(l => /Total DRC errors/i.test(l));
const match = summary?.match(/(\d+)/);
const violations = match ? parseInt(match[1], 10) : null;
return { violations, output };
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const { violations } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
console.log(`\nDRC violations: ${violations ?? '(count not found in output)'}`);
console.log('Done.');
}

5
npm/examples/drc.tcl Normal file
View File

@ -0,0 +1,5 @@
# DRC check runs design rule checking and reports violation count.
tech load __TECH__
load /work/__CELL__
drc catchup
drc count total

40
npm/examples/example.js Normal file
View File

@ -0,0 +1,40 @@
// example.js — GDS → CIF format conversion using the Magic WASM module.
//
// Usage:
// node examples/example.js [input.gds] [output.cif]
//
// Defaults:
// input: design.gds (in current working directory)
// output: design.cif (in current working directory)
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve, basename } from 'node:path';
import createMagicModule from '../magic.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const wasmBinary = readFileSync(resolve(__dirname, '../magic.wasm'));
const inputGds = process.argv[2] ?? 'design.gds';
const outputCif = process.argv[3] ?? inputGds.replace(/\.gds$/i, '.cif');
const cellName = basename(inputGds, '.gds');
const module = await createMagicModule({
wasmBinary,
print: msg => console.log('[magic]', msg),
printErr: msg => console.error('[magic]', msg),
});
module.FS.mkdirTree('/work');
module.FS.writeFile(`/work/${cellName}.gds`, readFileSync(inputGds));
module._magic_wasm_init();
module.cwrap('magic_wasm_run_command', 'number', ['string'])(`gds read /work/${cellName}`);
module.cwrap('magic_wasm_run_command', 'number', ['string'])(`load ${cellName}`);
module.cwrap('magic_wasm_run_command', 'number', ['string'])(`cif write /work/${cellName}`);
const cifBytes = module.FS.readFile(`/work/${cellName}.cif`);
writeFileSync(outputCif, cifBytes);
console.log(`Converted ${inputGds}${outputCif}`);

41
npm/examples/extract.js Normal file
View File

@ -0,0 +1,41 @@
// extract.js — RC extraction example (extract → extresist → ext2spice).
//
// Usage: node examples/extract.js [magFile [techFile [outputDir]]]
import { createMagic, vfsWrite, vfsRead, loadCell, loadScript,
DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers.js';
import { writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
export async function run({ magFile = DEFAULT_MAG, techFile = DEFAULT_TECH, outputDir = DEFAULT_OUT } = {}) {
const { magic } = await createMagic();
const { FS } = magic;
const { tech, cell } = loadCell(FS, techFile, magFile);
magic.runScript(loadScript('extract.tcl', tech, cell));
mkdirSync(outputDir, { recursive: true });
const extData = vfsRead(FS, `/work/${cell}.ext`);
if (!extData) throw new Error(`Extraction failed: /work/${cell}.ext not created`);
writeFileSync(resolve(outputDir, `${cell}.ext`), extData);
const resExtData = vfsRead(FS, `/work/${cell}.res.ext`);
if (resExtData) writeFileSync(resolve(outputDir, `${cell}.res.ext`), resExtData);
const spiceData = vfsRead(FS, `/work/${cell}.spice`) ?? vfsRead(FS, `/work/${cell}.spc`);
const spiceExt = vfsRead(FS, `/work/${cell}.spice`) ? 'spice' : 'spc';
if (spiceData) writeFileSync(resolve(outputDir, `${cell}.${spiceExt}`), spiceData);
return {
ext: resolve(outputDir, `${cell}.ext`),
spice: spiceData ? resolve(outputDir, `${cell}.${spiceExt}`) : null,
};
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const { ext, spice } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
console.log(`\next: ${ext}`);
if (spice) console.log(`spice: ${spice}`);
console.log('Done.');
}

27
npm/examples/extract.tcl Normal file
View File

@ -0,0 +1,27 @@
# extract.tcl
#
# Complete Magic extraction workflow:
# 1. Load technology
# 2. Load layout
# 3. Extract parasitic capacitances (extract all __CELL__.ext)
# 4. Extract parasitic resistances (extresist all __CELL__.res.ext)
# 5. Write SPICE netlist (ext2spice __CELL__.spice)
#
# __TECH__ and __CELL__ are substituted by extract.js before execution.
tech load __TECH__
load /work/__CELL__
# Write all intermediate files to /work/ so ext2spice can find __CELL__.res.ext
extract path /work
extract all
# extresist requires a valid box cursor; span the full layout to be safe
box 0 0 100000 100000
extresist all
ext2spice format ngspice
ext2spice extresist on
ext2spice cthresh 0
ext2spice rthresh 0
ext2spice /work/__CELL__

30
npm/examples/gds.js Normal file
View File

@ -0,0 +1,30 @@
// gds.js — Export layout to GDS II stream format.
//
// Usage: node examples/gds.js [magFile [techFile [outputDir]]]
import { createMagic, vfsRead, loadCell, loadScript,
DEFAULT_TECH, DEFAULT_MAG, DEFAULT_OUT } from './helpers.js';
import { writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
export async function run({ magFile = DEFAULT_MAG, techFile = DEFAULT_TECH, outputDir = DEFAULT_OUT } = {}) {
const { magic } = await createMagic();
const { FS } = magic;
const { tech, cell } = loadCell(FS, techFile, magFile);
magic.runScript(loadScript('gds.tcl', tech, cell));
mkdirSync(outputDir, { recursive: true });
const data = vfsRead(FS, `/work/${cell}.gds`);
if (!data) throw new Error(`GDS export failed: /work/${cell}.gds not created`);
const outPath = resolve(outputDir, `${cell}.gds`);
writeFileSync(outPath, data);
return { outPath, bytes: data.length };
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const { outPath, bytes } = await run().catch(e => { console.error(e.message ?? e); process.exit(1); });
console.log(`\ngds: ${outPath} (${bytes} bytes)`);
console.log('Done.');
}

4
npm/examples/gds.tcl Normal file
View File

@ -0,0 +1,4 @@
# GDS export
tech load __TECH__
load /work/__CELL__
gds write /work/__CELL__

72
npm/examples/helpers.js Normal file
View File

@ -0,0 +1,72 @@
// Shared utilities for Magic WASM examples.
import createMagicModule from '../magic.js';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve, basename } from 'node:path';
export const EXAMPLES_DIR = dirname(fileURLToPath(import.meta.url));
export const wasmBinary = readFileSync(resolve(EXAMPLES_DIR, '../magic.wasm'));
export const DEFAULT_TECH = resolve(EXAMPLES_DIR, 'siliwiz.tech');
export const DEFAULT_MAG = resolve(EXAMPLES_DIR, 'siliwiz.mag');
export const DEFAULT_OUT = resolve(EXAMPLES_DIR, 'output');
export { basename };
export function vfsWrite(FS, vfsPath, source) {
const dir = vfsPath.substring(0, vfsPath.lastIndexOf('/'));
if (dir) FS.mkdirTree(dir);
FS.writeFile(vfsPath, typeof source === 'string' ? readFileSync(source) : source);
}
export function vfsRead(FS, vfsPath) {
try { return Buffer.from(FS.readFile(vfsPath)); } catch { return null; }
}
export class MagicWasm {
constructor(mod) {
this._init = mod.cwrap('magic_wasm_init', 'number', []);
this._run = mod.cwrap('magic_wasm_run_command', 'number', ['string']);
this.FS = mod.FS;
}
init() {
const rc = this._init();
if (rc !== 0) throw new Error(`magic_wasm_init failed with code ${rc}`);
}
runScript(text) {
for (const line of text.split('\n')) {
const l = line.trim();
if (l && !l.startsWith('#')) this._run(l);
}
}
}
// 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.
export async function createMagic({ onPrint, onPrintErr } = {}) {
const lines = [];
const mod = await createMagicModule({
wasmBinary,
print: onPrint ?? (msg => { lines.push(msg); console.log('[magic]', msg); }),
printErr: onPrintErr ?? (msg => { lines.push(msg); console.error('[magic]', msg); }),
});
const magic = new MagicWasm(mod);
magic.init();
return { magic, lines };
}
// Loads tech + mag into VFS using standard paths.
export function loadCell(FS, techFile, magFile) {
const tech = basename(techFile, '.tech');
const cell = basename(magFile, '.mag');
vfsWrite(FS, `/magic/sys/current/${tech}.tech`, techFile);
vfsWrite(FS, `/work/${cell}.mag`, magFile);
return { tech, cell };
}
// Reads a TCL script from EXAMPLES_DIR, substitutes __TECH__ and __CELL__.
export function loadScript(name, tech, cell) {
return readFileSync(resolve(EXAMPLES_DIR, name), 'utf8')
.replaceAll('__TECH__', tech)
.replaceAll('__CELL__', cell);
}

51
npm/examples/siliwiz.mag Normal file
View File

@ -0,0 +1,51 @@
magic
tech siliwiz
magscale 1 1
timestamp 1776371010
<< nwell >>
rect 95 221 349 377
<< ndiffusion >>
rect 132 85 270 131
<< pdiffusion >>
rect 132 254 270 316
<< psubstratepdiff >>
rect 319 0 382 58
<< nsubstratendiff >>
rect 291 309 338 361
<< polysilicon >>
rect 178 51 217 327
rect 148 179 201 220
<< pdcontact >>
rect 233 264 266 307
rect 132 259 169 312
<< ndcontact >>
rect 236 89 268 127
rect 132 88 166 128
<< polycontact >>
rect 147 185 172 214
<< nsubstratencontact >>
rect 295 330 335 364
<< psubstratepcontact >>
rect 333 8 377 49
<< metal1 >>
rect 39 179 127 218
rect 270 180 366 218
rect 110 179 173 216
rect 229 89 270 307
rect 244 180 284 214
rect 128 264 164 366
rect 132 13 166 130
rect 40 9 374 49
rect 42 334 358 373
rect 29 328 80 377
rect 30 6 81 56
<< labels >>
flabel metal1 s 39 179 127 218 0 FreeSans 240 90 0 0 in
port 2 nsew signal output
flabel metal1 s 270 180 366 218 0 FreeSans 240 90 0 0 out
port 3 nsew signal output
flabel metal1 s 29 328 80 377 0 FreeSans 240 90 0 0 vdd
port 4 nsew signal output
flabel metal1 s 30 6 81 56 0 FreeSans 240 90 0 0 vss
port 5 nsew signal output
<< end >>

1452
npm/examples/siliwiz.tech Normal file

File diff suppressed because it is too large Load Diff

57
npm/index.d.ts vendored Normal file
View File

@ -0,0 +1,57 @@
export interface MagicInstance {
/**
* Initialize Magic (idempotent safe to call multiple times).
* Returns 0 on success, non-zero on failure.
*/
init(): number;
/**
* Dispatch a single Magic command string.
* Calls init() automatically on the first invocation.
* Returns 0 on success, non-zero on failure.
*/
runCommand(command: string): number;
/**
* Read and execute a command file at the given virtual filesystem path.
* Calls init() automatically on the first invocation.
* Returns 0 on success, -1 if the file could not be opened.
*/
sourceFile(path: string): number;
/**
* Drive a display-update cycle.
* No-op in headless builds the null display driver suspends all redraws.
*/
update(): void;
/**
* Emscripten virtual filesystem.
* Use FS.writeFile / FS.readFile to pass layout files in and out of Magic.
* See https://emscripten.org/docs/api_reference/Filesystem-API.html
*/
FS: any;
}
/**
* Create a Magic WASM instance.
*
* @param options Emscripten module options. Useful keys:
* - `wasmBinary` Pre-fetched ArrayBuffer of magic.wasm (avoids a second fetch)
* - `print` Callback for stdout lines (default: console.log)
* - `printErr` Callback for stderr lines (default: console.error)
*
* @example
* ```js
* import createMagic from 'magic-vlsi-wasm';
*
* const { runCommand, FS } = await createMagic();
* FS.writeFile('/work/inv.mag', layoutBytes);
* runCommand('tech load sky130A');
* runCommand('load /work/inv');
* runCommand('gds write /work/inv');
* const gds = FS.readFile('/work/inv.gds');
* ```
*/
export default function createMagic(options?: Record<string, unknown>): Promise<MagicInstance>;
export { createMagic };

15
npm/index.js Normal file
View File

@ -0,0 +1,15 @@
import MagicModuleFactory from './magic.js';
async function createMagic(options = {}) {
const module = await MagicModuleFactory(options);
const init = module.cwrap('magic_wasm_init', 'number', []);
const runCommand = module.cwrap('magic_wasm_run_command', 'number', ['string']);
const sourceFile = module.cwrap('magic_wasm_source_file', 'number', ['string']);
const update = module.cwrap('magic_wasm_update', null, []);
return { init, runCommand, sourceFile, update, FS: module.FS };
}
export { createMagic };
export default createMagic;

24
npm/pack.sh Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Pack the magic-vlsi-wasm npm tarball with reproducible-but-current timestamps.
#
# npm/pacote normalizes file mtimes for reproducibility and, when no
# SOURCE_DATE_EPOCH is set, falls back to 1985-10-26. That makes published
# tarballs carry 1985 dates, which confuses users and tooling.
#
# This script:
# 1. touches every file in npm/ so mtimes reflect the build time
# 2. sets SOURCE_DATE_EPOCH to `now` so pacote's normalization uses it
# 3. runs `npm pack`, producing magic-vlsi-wasm-<version>.tgz in npm/
#
# Used by:
# - npm/build.sh --pack (local build)
# - .github/workflows/main.yml (CI artifact upload)
# - .github/workflows/npm-publish.yml (tag-triggered publish)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
find . -exec touch {} +
SOURCE_DATE_EPOCH=$(date +%s) npm pack

36
npm/package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "magic-vlsi-wasm",
"version": "8.3.637",
"description": "Magic VLSI Layout Tool — headless WebAssembly build",
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"exports": {
".": {
"import": "./index.js",
"types": "./index.d.ts"
}
},
"files": [
"index.js",
"index.d.ts",
"magic.js",
"magic.wasm",
"examples/",
"LICENSE",
"README.md"
],
"scripts": {
"example": "node examples/extract.js",
"test": "node examples/all.js",
"test:gds": "node examples/gds.js",
"test:drc": "node examples/drc.js",
"test:cif": "node examples/cif.js"
},
"keywords": ["magic", "vlsi", "eda", "wasm", "webassembly", "layout"],
"license": "HPND",
"engines": {
"node": ">=18"
}
}