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:
parent
683bcfd867
commit
f5fa4d3603
|
|
@ -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
|
name: CI-aarch64
|
||||||
|
|
||||||
# Controls when the workflow will run
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
|
||||||
jobs:
|
jobs:
|
||||||
simple_build_linux_arm:
|
simple_build_linux_arm:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
|
|
@ -23,39 +24,3 @@ jobs:
|
||||||
./configure
|
./configure
|
||||||
make database/database.h
|
make database/database.h
|
||||||
make -j$(nproc)
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
# This is a basic workflow to help you get started with Actions
|
|
||||||
|
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
# Controls when the workflow will run
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
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:
|
jobs:
|
||||||
simple_build_linux:
|
simple_build_linux:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Get Dependencies
|
- name: Get Dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y tcl-dev tk-dev libcairo-dev
|
sudo apt-get install -y tcl-dev tk-dev libcairo-dev
|
||||||
|
|
@ -24,14 +26,24 @@ jobs:
|
||||||
make -j$(nproc)
|
make -j$(nproc)
|
||||||
simple_build_wasm:
|
simple_build_wasm:
|
||||||
runs-on: ubuntu-22.04
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Get Dependencies
|
- name: Get Dependencies
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/emscripten-core/emsdk.git
|
git clone https://github.com/emscripten-core/emsdk.git
|
||||||
cd emsdk
|
cd emsdk
|
||||||
./emsdk install latest
|
./emsdk install "$EMSDK_VERSION"
|
||||||
./emsdk activate latest
|
./emsdk activate "$EMSDK_VERSION"
|
||||||
|
# Dump native + emscripten preprocessor defines. Useful for diagnosing
|
||||||
|
# WASM-build differences after an emsdk bump.
|
||||||
- name: Emscripten Diagnostic
|
- name: Emscripten Diagnostic
|
||||||
run: |
|
run: |
|
||||||
source ./emsdk/emsdk_env.sh
|
source ./emsdk/emsdk_env.sh
|
||||||
|
|
@ -46,15 +58,36 @@ jobs:
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
source ./emsdk/emsdk_env.sh
|
source ./emsdk/emsdk_env.sh
|
||||||
# The --without and --disable in these build options is due to no WASM library being available for that feature
|
# --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 --target=asmjs-unknown-emscripten
|
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 ====="
|
echo "===== defs.mak ====="
|
||||||
cat defs.mak
|
cat defs.mak
|
||||||
echo "===== defs.mak ====="
|
echo "===== defs.mak ====="
|
||||||
emmake make
|
# Build in order: techs must exist before mains (--embed-file embeds them)
|
||||||
- name: archive wasm bundle
|
emmake make depend
|
||||||
uses: actions/upload-artifact@v4
|
emmake make -j$(nproc) modules libs
|
||||||
|
emmake make techs
|
||||||
|
emmake make mains
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
name: magic-wasm-bundle
|
node-version: '22'
|
||||||
path: |
|
- name: Run example tests
|
||||||
${{ github.workspace }}/magic/magic.wasm
|
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
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
magic.js
|
||||||
|
magic.wasm
|
||||||
|
*.tgz
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
examples/output/
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# CIF export
|
||||||
|
tech load __TECH__
|
||||||
|
load /work/__CELL__
|
||||||
|
cif write /work/__CELL__
|
||||||
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}`);
|
||||||
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
|
@ -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__
|
||||||
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# GDS export
|
||||||
|
tech load __TECH__
|
||||||
|
load /work/__CELL__
|
||||||
|
gds write /work/__CELL__
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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 >>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 };
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue