//.hxx"]
A3 --> A5["build/bindings////.d.ts.json"]
end
subgraph "Stage 2 — compile (em++)"
A4 --> B1[em++ -c per class]
B1 --> B2["build/bindings/.../.o (cached)"]
end
subgraph "Stage 3 — link (wasm-ld)"
B2 --> C1[wasm-ld + emcc glue]
C1 --> C2[dist/opencascade_full.wasm]
C1 --> C3[dist/opencascade_full.js]
A5 --> C4[dist/opencascade_full.d.ts]
end
`
```
## Stage 1 — bindgen [#stage-1--bindgen]
`scripts/build-wasm.sh bindings` invokes the Python bindgen which:
1. Walks every header listed in the active YAML via libclang.
2. Builds an AST per class, applies the `bindgen-filters.yaml` exclusions, and
classifies each member as constructor / static method / instance method /
property.
3. Resolves typedefs (including the OCCT `occ::handle<>` family) against a
shared cache.
4. Emits one `.hxx` per class with `EMSCRIPTEN_BINDINGS(...)` registrations and
one `.d.ts.json` shard per class describing the TypeScript shape.
The bindgen is deterministic: same headers + same filter YAML = identical
output bytes.
## Stage 2 — compile [#stage-2--compile]
`em++` compiles each `.hxx` to a `.o`. The cache key is the `.hxx` content
hash plus the compile-time flag fingerprint. Cached `.o` files survive across
consumer builds — that's the speedup that makes a custom-trimmed build take
60 seconds instead of 30 minutes.
## Stage 3 — link [#stage-3--link]
`emcc` links the selected `.o` files (per the consumer YAML's `bindings:`
list) against the pre-compiled OCCT library archives in `dist/libs/`. The
output:
* `.wasm` — the wasm binary.
* `.js` — the emscripten loader glue.
* `.d.ts` — the merged TypeScript declarations (from `.d.ts.json` shards
filtered to the bound symbol set).
* `.build-manifest.json` — symbol coverage report (requested vs compiled,
wasm bytes, validation flags).
## How custom C++ enters the pipeline [#how-custom-c-enters-the-pipeline]
`additionalCppCode` + `additionalCppFiles` get concatenated into one TU which
flows through a smaller variant of stages 1+2 — bindgen discovers Handle/NCollection
references, em++ compiles, the result links into the final wasm alongside the
auto-generated bindings.
`additionalBindCode` skips bindgen entirely — it's a literal `.cpp` snippet
that gets compiled with `` already included.
## When the pipeline fails [#when-the-pipeline-fails]
| Symptom | Stage | Fix |
| ------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `libclang: cannot find ` | 1 | OCCT headers not mounted into the Docker image |
| `undefined symbol: _ZN10TopoDS_...` | 3 | Remove a `bindings:` entry whose `.o` no longer exists, or the class transitively needs another class you trimmed |
| `BindingError: invalid type` at runtime | 3 | Duplicate `EMSCRIPTEN_BINDINGS()` group across `additionalBindCode` and a generated `.hxx` |
| Codegen emits `any` for a known type | 1 | The link step always prints a triage summary to stderr when this happens. The build still proceeds by default; set `OCJS_STRICT_TYPES=1` in CI to fail the build instead of shipping a poisoned `.d.ts`. File an issue with the printed triage summary. |
| Codegen emits `unknown` / surfaces an unbound reference | 1 | Same gate as above. Warning printed by default; `OCJS_STRICT_TYPES=1` escalates to a hard failure for CI consumers. |
## What the pipeline produces — JS-side contract [#what-the-pipeline-produces--js-side-contract]
The artifacts above are an implementation detail. The contract those artifacts
expose to JS consumers — overload-dispatched calls, in-place class outputs,
`returnValue` envelopes, Handle elision — lives in two consumer-facing
concept pages:
* [Calling OCCT from JS](/docs/toolchain/concepts/calling-occt-from-js) — overload dispatch, enums,
defaults, and the `TopoDS` downcast bridge.
* [Return shapes](/docs/toolchain/concepts/return-shapes) — class outputs, envelopes, `returnValue`,
and Handle elision.
## Docker stage mapping [#docker-stage-mapping]
The pipeline stages map directly to the published Docker stages described in
[Docker image](/docs/toolchain/reference/docker-image#image-stages):
| Pipeline stage | Docker stage | Published tag |
| ---------------------- | -------------------------------------------- | ------------------------------------- |
| 1 — discover | `bindgen-base` | `:bindgen-base` |
| 2 — emit (TUs + .d.ts) | `bindgen-base` | `:bindgen-base` |
| 3 — compile + link | `compiled-{threading}` + `final-{threading}` | `:single-threaded`, `:multi-threaded` |
`:bindgen-base` is both a build stage and a published image — it carries the
patched OCCT tree, the PCH, and the `.d.ts.json` index but not the
pre-compiled `.o` files. Custom-bindings consumers pull `:bindgen-base`,
re-run `generate` against their own YAML, and compile from there.
## Related [#related]
* [Extend with C++](/docs/toolchain/guides/extend-with-cpp) — how to inject custom C++ into the pipeline.
* [Trim symbols](/docs/toolchain/guides/trim-symbols) — controlling which classes survive into stage 3.
* [YAML schema](/docs/toolchain/reference/yaml-schema) — every YAML key the pipeline consumes.
---
# Two-channel config model
URL: /docs/toolchain/concepts/two-channel-config-model
**Maintainer track.** Skip this page if you consume the published npm
package — every shipped build already pins the right channel-1 flags. The
channels matter when you rebuild OCJS from source.
OpenCascade.js exposes two configuration channels with different lifecycles and
different scope. Treat them as orthogonal — confusion between the two is the
most common cause of build errors.
## Channel 1 — compile-time `OCJS_*` env vars [#channel-1--compile-time-ocjs_-env-vars]
Set at the **bindgen + C++ compile** stage. Bake into every `.o` file the
final wasm is linked from.
| Variable | Effect |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `OCJS_EXCEPTIONS=1` | Compile with `-fwasm-exceptions` everywhere |
| `OCJS_SIMD=1` | Compile with `-msimd128` |
| `OCJS_RELAXED_SIMD=1` | Additionally emit `-mrelaxed-simd` (Chrome/Firefox only) |
| `OCJS_BIGINT=1` | Compile with `-sWASM_BIGINT` |
| `OCJS_EVAL_CTORS_LEVEL=2` | `-sEVAL_CTORS=2` |
| `OCJS_STRICT_TYPES=1` | Escalate missing-typedef warning to a build failure (default `0`: warn-only — the triage summary is always printed to stderr) |
[^lto]: `OCJS_LTO` exists but is intentionally off in every shipped preset.
The workspace benchmarks showed LTO increasing the wasm size for OCJS's
object distribution, so it stays disabled. Flip it on locally only if
you're benchmarking a specific bindings trim.
These flags pin into a **build-flags manifest** alongside each cached `.o`.
Mixing builds with mismatched flags fails-loud at link time — never silent
ABI breakage.
## Channel 2 — link-time `emccFlags` [#channel-2--link-time-emccflags]
Set per-consumer-build in your YAML. Apply only to the final `emcc` link step.
```yaml
mainBuild:
emccFlags:
- -O3
- -sMODULARIZE=1
- -sEXPORT_ES6=1
- -sALLOW_MEMORY_GROWTH=1
```
These flags control loader shape (ESM vs CommonJS), memory limits, and
environment detection. They cannot retroactively change the exception model
or SIMD configuration the `.o` files were compiled with.
## Why the split? [#why-the-split]
The bindgen produces \~4,400 `.o` files in the maintainer Docker pipeline. Caching
them across consumer builds turns a 30-minute full build into a 60-second link.
The cache key must be deterministic — that's what the compile-time channel
fingerprint guards.
If consumers could change compile-time flags via YAML, the cache invariant
would break: a downstream `-fexceptions` request would silently rebuild every
class, defeating the cache. Splitting the channels makes the contract
explicit.
## When you actually need to change channel 1 [#when-you-actually-need-to-change-channel-1]
Almost never as a consumer. The published build is the named
`single-threaded` preset from `build-configs/configurations.json`, which pins:
* `OCJS_EXCEPTIONS=1` + `OCJS_EH_MODE=wasm` (native wasm exceptions)
* `OCJS_SIMD=1` (baseline SIMD, Safari-compatible)
* `OCJS_BIGINT=1` (no i64 legalisation)
* `OCJS_EVAL_CTORS=true` + `OCJS_EVAL_CTORS_LEVEL=2`
* `OCJS_CLOSURE=true` + `OCJS_CONVERGE=true`
* `THREADING=single-threaded`
Presets are addressed by name, not by raw env vars — see
[Named compile-time configurations](/docs/toolchain/reference/configurations) for the full
list (`single-threaded`, `single-threaded-smallest`, `multi-threaded`,
`debug`).
The combinations not yet pre-built are exotic — `OCJS_RELAXED_SIMD=1` for
Chrome-only deploys, custom allocator pairings, non-default WASM-opt budgets.
To get one, fork the Docker image build pipeline and rebuild from source.
## Diagnostic checklist [#diagnostic-checklist]
If a build acts strangely:
1. Confirm channel-1 fingerprint is what you expect. The full flag set is
recorded in the in-repo build manifest (regenerated by every `bindings`
stage):
```bash
cat build/build-flags.json
```
For the **published** tarball, the consumer-facing equivalent is
`dist/opencascade_full.provenance.json`, which captures the active preset,
compile flags, and commit SHA the wasm was built from.
2. Confirm channel-2 flags in the YAML actually made it into the wasm:
```bash
strings dist/my-build.wasm | grep -E 'STACK_SIZE|MAXIMUM_MEMORY'
```
3. Recompare against a known-good cached build — drift here points at
channel-1 cache poisoning (rebuild the deps layer).
---
# Quickstart — Docker
URL: /docs/toolchain/getting-started/quick-start-docker
For when you need a custom-trimmed `opencascade.wasm` without setting up emsdk,
libclang, and Python locally.
## 1. Pull the image [#1-pull-the-image]
```bash
docker pull ghcr.io/taucad/opencascade.js:single-threaded
```
The `:single-threaded` tag carries pre-compiled OCCT static libraries so your
build only pays for the link step. For threaded builds (requires COOP/COEP on
consumer pages), pull `:multi-threaded` instead. For custom-bindings work
that needs the pre-PCH/generate state but not the pre-compiled libraries, see
`:bindgen-base` in [Reference → Docker image](/docs/toolchain/reference/docker-image).
### Supported platforms [#supported-platforms]
Release tags (`:single-threaded`, `:multi-threaded`, `:bindgen-base`,
versioned variants) ship as **multi-architecture manifest lists** —
`linux/amd64` and `linux/arm64` — so Apple Silicon and ARM Linux hosts pull
the native arch automatically. No `--platform` flag required.
Branch tags (`:branch-`, `:multi-threaded-branch-`,
`:bindgen-base-branch-`) are **`linux/amd64` only** by design — branch
publishes need to be fast for reviewers, and the per-arch image build cost
is the dominant factor. ARM hosts pulling a branch tag will run under
Rosetta / QEMU. Use a release tag whenever native performance matters.
## 2. Write a build YAML [#2-write-a-build-yaml]
Create `mybuild.yml` next to your project — list the OCCT symbols you actually
use. See [Trim symbols](/docs/toolchain/guides/trim-symbols) for a worked example and
[YAML schema](/docs/toolchain/reference/yaml-schema) for the full schema.
```yaml title="mybuild.yml"
mainBuild:
name: my-occt
bindings:
- symbol: package
glob: 'gp'
- symbol: package
glob: 'BRepPrim*'
- symbol: package
glob: 'BRepAlgoAPI'
- symbol: package
glob: 'BRepBuilderAPI'
- symbol: package
glob: 'BRepFilletAPI'
- symbol: package
glob: 'TopoDS'
- symbol: package
glob: 'TopExp'
- symbol: package
glob: 'STEPControl'
- symbol: package
glob: 'RWGltf*'
emccFlags:
- -O3
- -fwasm-exceptions
- -sMODULARIZE=1
- -sEXPORT_ES6=1
- -sEXPORT_NAME=initOpenCascade
```
## 3. Run the image [#3-run-the-image]
```bash
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
ghcr.io/taucad/opencascade.js:single-threaded \
link mybuild.yml
```
`link` is the end-to-end command. Inside the container Nx's `dependsOn`
graph walks every upstream step (apply-patches → pch → generate →
compile-bindings → compile-sources → link) with cache reuse, so a fresh
container performs a full build and cached re-runs replay only the link
step.
Output lands next to `mybuild.yml`:
* `my-occt.wasm` — the trimmed wasm binary
* `my-occt.js` — the emscripten loader
* `my-occt.d.ts` — generated TypeScript declarations
* `my-occt.build-manifest.json` — symbol coverage and size report
## 4. Wire the build output [#4-wire-the-build-output]
```typescript
import init from './my-occt.js';
import wasmUrl from './my-occt.wasm?url';
const oc = await init({ locateFile: () => wasmUrl });
```
## 5. Pin by manifest-list digest in production [#5-pin-by-manifest-list-digest-in-production]
Tags can be re-pointed; digests cannot. Resolve the manifest-list digest
once and pin it in CI so every reproducer of your build pulls the exact
same multi-arch image set:
```bash
docker buildx imagetools inspect ghcr.io/taucad/opencascade.js:single-threaded \
--format '{{json .Manifest.Digest}}'
# → "sha256:abc123…"
# Pin in CI / docker-compose / Dockerfile:
ghcr.io/taucad/opencascade.js@sha256:abc123…
```
The manifest-list digest covers both `linux/amd64` and `linux/arm64`
sub-images — pulling by digest from any arch resolves to the same
deterministic build.
See [Reproducible CI](/docs/toolchain/guides/reproducible-ci) for the full workflow.
## 6. Verify the supply-chain signature (optional) [#6-verify-the-supply-chain-signature-optional]
Every published image is signed with [cosign](https://github.com/sigstore/cosign)
via OIDC keyless signing — no rotating private keys, signatures published to
the Sigstore Rekor transparency log. To verify before pulling:
```bash
cosign verify ghcr.io/taucad/opencascade.js:single-threaded \
--certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
A successful verification confirms the image was built by the
`taucad/opencascade.js` GitHub Actions workflow and has not been tampered
with since publication.
## Where the time goes [#where-the-time-goes]
Typical wall-clock for a 22-50 symbol trim on the GitHub Actions `ubuntu-latest`
runner (the e2e gate budget; M1 Pro local times are 30-50% faster):
| Step | Warm cache | Cold cache |
| -------------------------------------- | ------------: | ------------: |
| Image pull (compressed \~1.3 GB) | 0 s | 30-60 s |
| `generate` against your YAML | 5-15 s | 5-15 s |
| `compile-bindings` (cached `.o` files) | 0 s | 1-3 min |
| `compile-sources` (cached OCCT `.a`) | 0 s | 5-10 min |
| Link against precompiled OCCT objects | 30-90 s | 30-90 s |
| **Total** | **\~1-2 min** | **\~3-5 min** |
The CI e2e gate budgets a hard 5-minute warm-link ceiling and tags get
published only if both `:single-threaded` and `:multi-threaded` clear that
budget on both architectures. Cold-cache runs (first pull on a new machine)
add the image pull time; subsequent runs on the same machine reuse the
pulled image and Nx cache.
Full surface builds (no symbol filter, i.e. `build-configs/full.yml`) take
25-40 minutes — they're for the maintainer pipeline, not consumer machines.
---
# Custom emcc flags
URL: /docs/toolchain/guides/custom-emcc-flags
Once your YAML symbol list is right, the second knob is `emccFlags`. Use it to
tune optimisation level, exception model, SIMD, and target environments.
## Recommended baseline [#recommended-baseline]
```yaml
mainBuild:
emccFlags:
- -O3
- -fwasm-exceptions # native wasm exceptions; faster than JS exceptions
- -msimd128 # baseline SIMD (Safari + everyone else)
- -sWASM_BIGINT # i64 stays as BigInt, no legalisation glue
- -sEVAL_CTORS=2 # static-init evaluation at build time
- -sMODULARIZE=1 # init() returns Promise
- -sEXPORT_ES6=1 # ESM output
- -sENVIRONMENT=web,worker,node
- -sALLOW_MEMORY_GROWTH=1
- -sMAXIMUM_MEMORY=4GB # wasm32 ceiling
```
## Flag-by-flag rationale [#flag-by-flag-rationale]
| Flag | Why |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `-O3` | Full LLVM optimisation. `-Os` is \~5% smaller, \~10% slower at runtime. `-O0` for debugging only. |
| `-fwasm-exceptions` | Uses Wasm `try_table`. \~5–10% smaller and faster than `-fexceptions` (JS-based) when supported (every modern engine). |
| `-msimd128` | Baseline SIMD. \~15% perf win on mesh and boolean kernels. Safari-compatible. |
| `-mrelaxed-simd` | Chrome/Firefox-only additional SIMD ops. Adds another \~5% perf but breaks Safari — only ship if you can fall back. |
| `-sWASM_BIGINT` | Removes the i64↔i32-pair shim. Saves \~30 KB JS glue. |
| `-sEVAL_CTORS=2` | Runs static initialisers at build time. Smaller payload + faster startup. |
| `-sMODULARIZE=1 -sEXPORT_ES6=1` | Required for `import init from '...'`. |
| `-sENVIRONMENT=web,worker,node` | Strips dead env detection. Without this the runtime probes for `process`/`window`/`importScripts`. |
| `-sALLOW_MEMORY_GROWTH=1` | Required for any non-trivial geometry — initial 16 MB heap is not enough. |
| `-sMAXIMUM_MEMORY=4GB` | wasm32 hard ceiling (2³² bytes). |
## Trade-offs [#trade-offs]
### Wasm exceptions vs JS exceptions [#wasm-exceptions-vs-js-exceptions]
`-fwasm-exceptions` requires that **all** `.o` files AND the linker use the flag
consistently. Mixed builds produce `__cpp_exception` as an unresolved import at
link time. The default OCJS build sets `OCJS_EXCEPTIONS=1` everywhere; only
override if you have a very narrow constraint (e.g. a wasm engine without
`try_table` support).
### SIMD: baseline vs relaxed [#simd-baseline-vs-relaxed]
Browsers diverged. Baseline `-msimd128` is universal. Relaxed-SIMD
(`-mrelaxed-simd`) is Chrome + Firefox at the time of writing — Safari (as of
26.x) refuses to parse the relaxed opcodes and the wasm module fails to
instantiate. Ship relaxed-SIMD only if you maintain a fallback baseline build.
### EVAL\_CTORS levels [#eval_ctors-levels]
| Level | Behaviour |
| ----- | --------------------------------------------------- |
| 0 | Off — every static init runs at startup |
| 1 | Eval ctors with safe side effects |
| 2 | Recommended — full ctor evaluation, requires `-O2`+ |
`EVAL_CTORS=2` is the OCJS shipped default and the right answer almost always.
## What you can NOT change at link time [#what-you-can-not-change-at-link-time]
The wasm bitwidth (`wasm32` vs `wasm64`), the C++ stdlib version, the OCCT
commit pin, and the libclang version are all baked in during the bindgen
pipeline that produces the published Docker image. Customise via a fork of the
Docker image if you need any of those.
---
# Derive a C++ class in JavaScript
URL: /docs/toolchain/guides/derive-cpp-class-in-js
OpenCascade reports progress and supports user-initiated cancellation through
`Message_ProgressIndicator`. Because OCCT requires a **derived class** with
overridden virtual methods, using progress callbacks in OpenCascade.js requires
a **custom WASM build** that exposes `allow_subclass<>` bindings.
This guide consolidates the legacy three-page `progress-indicators-user-break`
series into a single V3 tutorial.
## Overview [#overview]
1. Add a C++ bridge struct and `EMSCRIPTEN_WRAPPER` in your custom build YAML.
2. Register `allow_subclass<>` in `additionalBindCode`.
3. Derive the class in JavaScript with `.extend()` and pass `Start()` into
long-running algorithms like `BRepAlgoAPI_Fuse`.
Three methods matter:
| Method | Required | Purpose |
| ----------- | -------- | ------------------------------------------------------ |
| `Show` | Yes | Called on every progress update (pure virtual in OCCT) |
| `UserBreak` | Optional | Return `true` to cancel the current operation |
| `Reset` | Optional | Called when a new long-running process starts |
## Step 1 — Custom build bindings [#step-1--custom-build-bindings]
Create `wrappers/progress-indicator-js.cpp`:
```cpp title="wrappers/progress-indicator-js.cpp"
#include
#include
#include
using namespace emscripten;
struct Message_ProgressIndicator_JS : public Message_ProgressIndicator {
using Message_ProgressIndicator::Show;
using Message_ProgressIndicator::UserBreak;
using Message_ProgressIndicator::Reset;
};
struct Message_ProgressIndicator_JSWrapper : public wrapper {
EMSCRIPTEN_WRAPPER(Message_ProgressIndicator_JSWrapper);
void Show(const Message_ProgressScope& theScope, const Standard_Boolean isForce) {
val valTheScope = val::object();
valTheScope.set("current", &theScope);
return call("Show", valTheScope, isForce);
}
Standard_Boolean UserBreak() { return call("UserBreak"); }
void Reset() { return call("Reset"); }
};
```
Add to your build YAML:
```yaml title="build-configs/with-progress-callback.yml"
additionalCppFiles:
- wrappers/progress-indicator-js.cpp
mainBuild:
bindings:
- symbol: Message_ProgressIndicator
- symbol: Message_ProgressScope
- symbol: Message_ProgressRange
- symbol: BRepAlgoAPI_Fuse
- symbol: BRepPrimAPI_MakeBox
- symbol: gp_Pnt
additionalBindCode: |
EMSCRIPTEN_BINDINGS(progress_indicator_js) {
class_>(
"Message_ProgressIndicator_JS")
.function("Show", &Message_ProgressIndicator_JS::Show, pure_virtual())
.function("UserBreak", optional_override([](Message_ProgressIndicator_JS& self) {
return self.Message_ProgressIndicator_JS::UserBreak();
}))
.function("Reset", optional_override([](Message_ProgressIndicator_JS& self) {
return self.Message_ProgressIndicator_JS::Reset();
}))
.allow_subclass("Message_ProgressIndicator_JSWrapper");
}
```
Build with the [Toolchain quickstart](/docs/toolchain/getting-started/quick-start-docker).
## Step 2 — Derive in JavaScript [#step-2--derive-in-javascript]
```typescript title="examples/progress-indicator.ts"
import { getOc } from './ocjs-init';
let shouldCancel = false;
export const runFuseWithProgress = async (): Promise => {
const oc = await getOc();
const MyProgress = oc.Message_ProgressIndicator_JS.extend('Message_ProgressIndicator_JS', {
Show(_scope, _isForce) {
console.log('progress', this.GetPosition());
},
UserBreak() {
return shouldCancel;
},
});
using p = new MyProgress();
using box1 = new oc.BRepPrimAPI_MakeBox(new oc.gp_Pnt(0, 0, 0), 2, 1, 1);
using box2 = new oc.BRepPrimAPI_MakeBox(new oc.gp_Pnt(1, 0, 0), 2, 1, 1);
using fuse = new oc.BRepAlgoAPI_Fuse(box1.Shape(), box2.Shape(), p.Start());
fuse.Build(new oc.Message_ProgressRange());
};
```
V3 uses suffix-free `Start()` — the v2 `_1` / `_2` overload suffixes no longer
exist. Overload dispatch happens in C++ via the unified RBV pipeline.
## Step 3 — Cancellation [#step-3--cancellation]
Set `shouldCancel = true` from a UI button or timeout. The next `UserBreak()`
call returns `true` and OCCT aborts the in-flight operation.
See also: [Extend with C++](/docs/toolchain/guides/extend-with-cpp) for the general custom-build
mechanics, and [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions)
if the derived class throws across the wasm boundary.
---
# Extend with C++
URL: /docs/toolchain/guides/extend-with-cpp
OCCT exposes hundreds of free functions, POD structs, and helper utilities the
bindgen does not (and cannot) reach automatically. When you need one of them —
or want an ergonomic wrapper around an OCCT API — the YAML config gives you
three mechanisms:
1. **`additionalCppCode`** — inline C++ embedded directly in the YAML scalar.
2. **`additionalCppFiles`** — one or more `.cpp` files concatenated onto `additionalCppCode`.
3. **`mainBuild.additionalBindCode`** — per-build raw `EMSCRIPTEN_BINDINGS(...)` registrations.
## Decision tree [#decision-tree]
```text
Are you adding new C++ implementation code (wrapper class, free function, POD)?
├── YES → Will the impl grow beyond ~100 lines or want syntax highlighting?
│ ├── YES → additionalCppFiles
│ └── NO → additionalCppCode (inline)
└── NO → You are only adding raw embind registrations on existing symbols
→ mainBuild.additionalBindCode
```
The three compose. A typical custom binding ships a `.cpp` file
(`additionalCppFiles`) plus an `EMSCRIPTEN_BINDINGS(...)` block
(`additionalBindCode`) that registers what the `.cpp` defined.
## Mechanism 1 — `additionalCppCode` (inline) [#mechanism-1--additionalcppcode-inline]
For short snippets that belong with the YAML for context — handle typedefs, a
single free function, a small POD.
```yaml
additionalCppCode: |
#include
Standard_Real addReals(Standard_Real a, Standard_Real b) { return a + b; }
mainBuild:
name: my_build
additionalBindCode: |
EMSCRIPTEN_BINDINGS(my_build_extras) {
emscripten::function("addReals", &addReals);
}
```
Now `oc.addReals(1.5, 2.25)` returns `3.75`.
## Mechanism 2 — `additionalCppFiles` (multi-file) [#mechanism-2--additionalcppfiles-multi-file]
When the wrapper grows past \~100 lines or you want real `.cpp` files for editor
support and review, move it to `additionalCppFiles`. The files are concatenated
onto `additionalCppCode` (in that order) into one translation unit before custom-binding
generation runs.
```yaml
additionalCppFiles:
- wrappers/fair-curve.cpp
mainBuild:
additionalBindCode: |
EMSCRIPTEN_BINDINGS(my_build_faircurve) {
emscripten::function("computeFairCurve", &computeFairCurve);
}
```
Paths in `additionalCppFiles` are resolved relative to the YAML file's
directory. A missing file fails-loud at validate time.
## Mechanism 3 — `additionalBindCode` (raw embind) [#mechanism-3--additionalbindcode-raw-embind]
Use this when bindgen cannot emit the registration you need. Common cases:
* Binding a free function (bindgen registers classes, not free functions).
* Defining a `value_object` POD authored in `additionalCppCode` / `additionalCppFiles`.
* Registering an `emscripten::vector` / `emscripten::map` an NCollection
auto-discovery pass does not cover.
```yaml
additionalCppCode: |
struct PointXY { double X; double Y; };
mainBuild:
additionalBindCode: |
EMSCRIPTEN_BINDINGS(my_build_pointxy) {
emscripten::value_object("PointXY")
.field("X", &PointXY::X)
.field("Y", &PointXY::Y);
}
```
Every `EMSCRIPTEN_BINDINGS()` group **must use a unique name** across the
block and any auto-generated binding TUs — embind enforces uniqueness at module
load time.
## Worked example — wrapper class with constructor + methods [#worked-example--wrapper-class-with-constructor--methods]
```cpp title="wrappers/shape-cast.cpp"
#include
#include
#include
#include
class ShapeCast {
public:
static TopoDS_Edge toEdge(const TopoDS_Shape& s) { return TopoDS::Edge(s); }
static TopoDS_Face toFace(const TopoDS_Shape& s) { return TopoDS::Face(s); }
};
```
```yaml title="build-configs/my-config.yml"
additionalCppFiles:
- wrappers/shape-cast.cpp
mainBuild:
bindings:
- symbol: TopoDS_Shape
- symbol: TopoDS_Edge
- symbol: TopoDS_Face
additionalBindCode: |
EMSCRIPTEN_BINDINGS(my_build_shape_cast) {
emscripten::class_("ShapeCast")
.class_function("toEdge", &ShapeCast::toEdge)
.class_function("toFace", &ShapeCast::toFace);
}
```
JS:
```typescript
const edge = oc.ShapeCast.toEdge(genericShape);
```
## Pitfalls [#pitfalls]
* **Duplicate `EMSCRIPTEN_BINDINGS` group names** crash the module at load time.
* **Forgetting to bind the underlying OCCT class** in `bindings:` causes the
wrapper to compile but the runtime cast to throw.
* **Using `Handle` without the typedef** works in C++ but produces no useful
JS shape. Add `typedef opencascade::handle Handle_T;`.
## Related [#related]
* [YAML schema](/docs/toolchain/reference/yaml-schema) — schema for every YAML key.
* [Emscripten embind reference](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html).
---
# Custom multi-threaded build
URL: /docs/toolchain/guides/multi-threading
The npm package already ships a pre-built multi-threaded variant at `@taucad/opencascade.js/multi` — see [Package — Multi-threaded build](/docs/package/guides/multi-threading) for the import recipe, COOP/COEP headers, and OCCT activation calls. Rebuild from source only when you need:
* A trimmed symbol list (the shipped MT binary builds from `build-configs/full_multi.yml` with the same \~4,400 symbols as the ST binary).
* Custom `emccFlags` (alternative `STACK_SIZE`, `INITIAL_MEMORY`, capped `PTHREAD_POOL_SIZE`).
* A relaxed-SIMD sibling for non-Safari deployments (see the callout below).
## Build YAML [#build-yaml]
```yaml title="build-configs/threaded.yml"
mainBuild:
name: occt_threaded
bindings:
# ... your symbol list ...
emccFlags:
- -O3
- -fwasm-exceptions
- -msimd128
- -pthread
- -sUSE_PTHREADS=1
# Pre-spawn one worker per logical CPU on the host. The expression is
# evaluated by the generated JS glue at module-instantiation time, so a
# single binary adapts to whatever hardware loads it.
- -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency
- -sSHARED_MEMORY=1
- -sMODULARIZE=1
- -sEXPORT_ES6=1
- -sENVIRONMENT=web,worker,node
- -sALLOW_MEMORY_GROWTH=1
- -sMAXIMUM_MEMORY=4GB
```
Build with:
```bash
./build-wasm.sh --config multi-threaded full build-configs/threaded.yml
```
`PTHREAD_POOL_SIZE=navigator.hardwareConcurrency` evaluates per-host: an M2
Pro browser gets 12 workers, a mobile device gets 4–6, a Node server gets
whatever `os.cpus().length` reports. Each worker costs `STACK_SIZE` (default
8 MB) at module init, so a 12-core box reserves \~96 MB of stack on top of
`INITIAL_MEMORY`. If you need a hard cap, wrap the expression:
`Math.min(navigator.hardwareConcurrency, 16)`.
> ### Why not a small fixed pool size? [#why-not-a-small-fixed-pool-size]
>
> A round-number pool like `-sPTHREAD_POOL_SIZE=4` is tempting but
> silently caps your speedup. On a 12-core machine OCCT's
> `OSD_ThreadPool` requests 11 workers (`NbLogicalProcessors-1`); with a
> pool of 4 the parallel sections degrade to 4-way fan-out and the
> headline speedup stalls around 1.3×. Sizing the pool to
> `navigator.hardwareConcurrency` recovers the full 2.0–2.5× on
> mesh-dominated workloads, and shrinks gracefully on lower-core devices
> without a separate build.
> ### Relaxed-SIMD — advanced, non-default [#relaxed-simd--advanced-non-default]
>
> Do **not** add `-mrelaxed-simd` to the binary you ship to
> browsers. Safari / WebKit (all versions as of May 2026) crash the JIT
> when a relaxed-SIMD opcode actually executes — validation passes but
> execution does not. If you want the 5–15 % uplift on Chromium /
> Firefox / Node, build a sibling `occt_threaded_rsimd.wasm` with
> `-mrelaxed-simd` and `wasm-opt --enable-relaxed-simd`, then UA-sniff in
> your loader to serve the plain binary to WebKit and the relaxed
> binary to everyone else.
> ### `EVAL_CTORS=2` is dropped under threading [#eval_ctors2-is-dropped-under-threading]
>
> `full.yml` enables `-sEVAL_CTORS=2` for the single-threaded build, but
> `full_multi.yml` drops it — constructor evaluation order is
> non-deterministic under pthread workers. The trade-off is a slightly
> larger binary in exchange for race-free startup.
>
> **Companion observation:** the `wasm-opt` post-link pass reports only
> \~0.1% size reduction on MT builds vs the 4-7% it achieves on ST. That
> is **not** a regression — `wasm-opt` would normally fold static
> ctor calls during the same pass `EVAL_CTORS=2` enables; with that
> flag dropped, the constructors stay live and wasm-opt has less to
> eliminate. No action required.
## Browser-only MT build [#browser-only-mt-build]
If you can guarantee your consumers run a recent browser (Chrome / Edge ≥ 144, Firefox ≥ 145, Safari ≥ 26.2 — May 2026 baseline) and you do **not** need to load the binary under Node.js, Bun, or Deno, you can opt into the `multi-threaded-browser` preset for a measurable runtime win on memory-growth-heavy workloads.
The preset layers `-sGROWABLE_ARRAYBUFFERS=1` and `-sENVIRONMENT=web,worker` on top of the standard `multi-threaded` flag set. Internally Emscripten emits JS glue that wraps the pthread `SharedArrayBuffer` in a [resizable `ArrayBuffer` view](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/grow) via `WebAssembly.Memory.prototype.toResizableBuffer()`. With that view in place, consumers can hoist `HEAP*` references out of hot loops — they no longer go stale after a memory-growth event, so the `-Wpthreads-mem-growth` link-time advisory disappears entirely and per-call HEAP re-fetches drop out of the inner loop.
Build with:
```bash
./build-wasm.sh --config multi-threaded-browser full build-configs/full_multi_browser.yml
```
The shipped recipe lives at [`build-configs/full_multi_browser.yml`](https://github.com/taucad/opencascade.js/blob/main/build-configs/full_multi_browser.yml); diff it against [`full_multi.yml`](https://github.com/taucad/opencascade.js/blob/main/build-configs/full_multi.yml) to see the exact two-flag delta.
> ### Runtime support matrix — `toResizableBuffer()` (May 2026) [#runtime-support-matrix--toresizablebuffer-may-2026]
>
> **Supported** — ship `full_multi_browser` artefacts to:
>
> * Chrome / Edge ≥ 144
> * Firefox ≥ 145
> * Safari ≥ 26.2 (desktop + iOS)
>
> **NOT supported** — fall back to plain `full_multi` for:
>
> * Node.js (all current LTS) — throws `TypeError: wasmMemory.toResizableBuffer is not a function` on module init.
> * Bun and Deno (all current versions).
> * Samsung Internet and other Chromium derivatives that lag mainline.
>
> Because Node.js is the dominant test-harness runtime, the **default**
> shipped multi-threaded build keeps `-sGROWABLE_ARRAYBUFFERS=0` so it
> stays loadable under `vitest`, `jest`, and other Node-based test runners.
> Opt into the browser variant only for production payloads you serve
> directly to browser clients.
## After building [#after-building]
Wire the resulting `.js` / `.wasm` triple into your bundler the same way as the shipped variant — see [Package — Multi-threaded build](/docs/package/guides/multi-threading) for activation calls, parallel-aware OCCT APIs, and benchmarks.
## Related [#related]
* [Configurations reference](/docs/toolchain/reference/configurations) — `multi-threaded` preset and how to author a custom one.
* [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) — link-time `emccFlags` rationale.
* [Trim symbols](/docs/toolchain/guides/trim-symbols) — shrink the symbol set before building.
---
# Reproducible CI
URL: /docs/toolchain/guides/reproducible-ci
A reproducible OCJS build means: same inputs, same wasm bytes, every time.
Three controls close the loop.
## 1. Pin the image by manifest-list digest [#1-pin-the-image-by-manifest-list-digest]
`:single-threaded`, `:multi-threaded`, and `:bindgen-base` are moving tags
that roll forward with every release. CI should pin by digest so a new
publish doesn't silently change your wasm output. Pinning the manifest-list
digest (not the per-arch image digest) covers both `linux/amd64` and
`linux/arm64` sub-images in a single deterministic pin.
```bash
# Resolve the manifest-list digest once
docker buildx imagetools inspect ghcr.io/taucad/opencascade.js:single-threaded \
--format '{{json .Manifest.Digest}}'
# → "sha256:abc123…"
# Use the digest in CI (resolves to the matching native arch transparently)
docker pull ghcr.io/taucad/opencascade.js@sha256:abc123...
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
ghcr.io/taucad/opencascade.js@sha256:abc123... \
link mybuild.yml
```
## 2. Verify the cosign signature [#2-verify-the-cosign-signature]
Every published image is signed via cosign keyless signing (OIDC) of the
manifest-list digest — one signature verifies regardless of which arch
the consumer pulls.
```bash
cosign verify ghcr.io/taucad/opencascade.js@sha256:abc123... \
--certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
A successful verification confirms the image was built by the
`taucad/opencascade.js` GitHub Actions `docker.yml` workflow and has not
been tampered with since publication. CI failing this step on a sudden
mismatch is a smoking gun that something tampered with the supply chain.
## 2b. Verify SLSA provenance attestation [#2b-verify-slsa-provenance-attestation]
In addition to the cosign signature, the image ships a SLSA provenance
attestation:
```bash
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp 'https://github\.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/taucad/opencascade.js@sha256:abc123...
```
The attestation records the source commit, the runner, and the workflow
that produced the image.
## 3. Extract SBOM [#3-extract-sbom]
```bash
docker buildx imagetools inspect \
--format '{{ json .SBOM }}' \
ghcr.io/taucad/opencascade.js@sha256:abc123...
```
The SBOM lists every apt package and pinned commit (OCCT, freetype, rapidjson)
embedded in the image. Diff it against the prior digest's SBOM in CI to flag
unexpected dep bumps.
## 4. Lock downstream consumers [#4-lock-downstream-consumers]
In your `package.json`, pin the npm tarball alongside the docker digest:
```json
{
"dependencies": {
"@taucad/opencascade.js": "3.0.0-beta.5"
},
"pnpm": {
"supportedArchitectures": {
"os": ["linux", "darwin"],
"cpu": ["x64", "arm64"]
}
}
}
```
Commit `pnpm-lock.yaml`. The lockfile records the integrity hash of the
published tarball; if it ever changes, `pnpm install --frozen-lockfile` fails
in CI.
## End-to-end CI snippet [#end-to-end-ci-snippet]
```yaml title=".github/workflows/wasm-build.yml"
jobs:
build-wasm:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Verify cosign signature
run: |
DIGEST=$(cat .ocjs-digest)
cosign verify "ghcr.io/taucad/opencascade.js@${DIGEST}" \
--certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
- name: Verify image provenance
run: |
DIGEST=$(cat .ocjs-digest)
cosign verify-attestation --type slsaprovenance \
--certificate-identity-regexp 'https://github\.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"ghcr.io/taucad/opencascade.js@${DIGEST}"
- name: Build wasm
run: |
DIGEST=$(cat .ocjs-digest)
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
"ghcr.io/taucad/opencascade.js@${DIGEST}" \
link build-configs/my-config.yml
- name: Assert wasm hash
run: |
EXPECTED=$(cat .wasm-hash)
ACTUAL=$(sha256sum my-config.wasm | cut -d' ' -f1)
[ "$EXPECTED" = "$ACTUAL" ] || { echo "wasm hash drift"; exit 1; }
```
The trailing `assert wasm hash` step is the safety net. If `EXPECTED` and
`ACTUAL` ever diverge, something in the published image changed — either
intentionally (bump your `.wasm-hash`) or by accident (investigate).
---
# Trim symbols
URL: /docs/toolchain/guides/trim-symbols
`opencascade_full.wasm` binds \~4,400 OCCT classes and weighs roughly 27 MB.
Most consumers need a fraction of that surface — a STEP round-tripper might
touch \~120 classes, a glTF mesher even fewer. Trimming the YAML symbol list
shrinks the wasm payload and reduces startup time.
This guide drives the entire workflow through the published Docker image so
you do not need `emsdk`, `libclang`, or Python installed locally. The image
ships with `emsdk`, `libclang`, Python, the precompiled OCCT object cache,
and the reference `full.yml` already in place.
## Prerequisites [#prerequisites]
* Docker (or any OCI runtime — Colima, Rancher Desktop, OrbStack all work).
* A working directory under your home folder (`/Users/...` on macOS,
`C:\Users\...` on Windows). Other top-level folders like `/tmp` and
`/opt` are not shared into the Docker VM by default and will silently
drop build outputs.
* Familiarity with the YAML schema. See [YAML schema](/docs/toolchain/reference/yaml-schema)
for the full reference.
If you have not used the image before, run the
[Quickstart — Docker](/docs/toolchain/getting-started/quick-start-docker) once to verify
your setup before trimming.
## Size budget [#size-budget]
Each bound class contributes roughly 15–25 KB to the linked wasm. A
reasonable target by use case:
| Use case | Symbols | Approximate wasm size |
| ----------------------------------------------- | -----------------: | --------------------: |
| Single-format viewer (read STEP → mesh) | 80–150 | 2–4 MB |
| Round-trip pipeline (STEP/IGES edit + write) | 200–400 | 5–10 MB |
| Full code-CAD tool (booleans + fillets + sweep) | 600–1,200 | 12–20 MB |
| Reference / kitchen sink | 4,400 (`full.yml`) | \~27 MB |
Numbers are after `-O3 -msimd128 -sWASM_BIGINT -sEVAL_CTORS=2`.
## Steps [#steps]
### 1. Extract the reference `full.yml` [#1-extract-the-reference-fullyml]
The image ships the reference `full.yml` at
`/opencascade.js/build-configs/full.yml`. Pull it out with an entrypoint
override so you can use it as your starting point:
```bash
docker run --rm --entrypoint cat \
ghcr.io/taucad/opencascade.js:single-threaded \
/opencascade.js/build-configs/full.yml \
> my-config.yml
```
If you already know exactly which classes you need you can also hand-write
`my-config.yml` from scratch — see the worked example below.
### 2. Delete the classes you don't need [#2-delete-the-classes-you-dont-need]
Open `my-config.yml` and remove any `bindings` entry you don't call from
JavaScript. There is no transitive closure to compute — Handle typedefs
and `NCollection_*` members for the surviving classes are auto-discovered
at codegen time, so you only list classes you instantiate or pass
references to.
### 3. Validate [#3-validate]
```bash
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
ghcr.io/taucad/opencascade.js:single-threaded \
validate /src/my-config.yml
```
`validate` parses the YAML against the schema and fails non-zero on any
malformed entry, unknown key, or duplicated symbol. It does not run the
C++ pipeline, so it returns in under a second.
### 4. Link [#4-link]
```bash
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
ghcr.io/taucad/opencascade.js:single-threaded \
link my-config.yml
```
`link` reuses the precompiled `.o` files baked into the image for every
still-bound class, so a trim that strips half of `full.yml` typically
completes in 60–180 seconds.
Output lands next to `my-config.yml` (per `OCJS_OUTPUT_DIR=/src` default):
* `.wasm` — the trimmed wasm binary
* `.js` — the emscripten loader
* `.d.ts` — generated TypeScript declarations
* `.build-manifest.json` — symbol coverage and size report
If you removed a class another bound class transitively needs, the link
step fails with `undefined symbol`. Add it back and re-run.
### 5. Iterate with a persistent cache [#5-iterate-with-a-persistent-cache]
Repeated trims benefit from caching the Nx graph between runs. Create a
named volume once, then mount it on every subsequent invocation:
```bash
docker volume create ocjs-nx-cache
docker run --rm \
-v ocjs-nx-cache:/opencascade.js/.nx \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
ghcr.io/taucad/opencascade.js:single-threaded \
link my-config.yml
```
Cache hits drop subsequent link times to \~10–30 seconds when only the
YAML changed and the touched classes were already compiled.
## Worked example: STEP round-tripper [#worked-example-step-round-tripper]
```yaml title="my-config.yml"
mainBuild:
name: step_roundtrip
bindings:
- symbol: Standard_Failure
- symbol: Message_ProgressRange
- symbol: TCollection_AsciiString
- symbol: TCollection_ExtendedString
- symbol: TopoDS
- symbol: TopoDS_Shape
- symbol: TopoDS_Compound
- symbol: TopoDS_Solid
- symbol: TopoDS_Shell
- symbol: TopoDS_Face
- symbol: TopoDS_Wire
- symbol: TopoDS_Edge
- symbol: TopoDS_Vertex
- symbol: TopExp
- symbol: TopExp_Explorer
- symbol: gp_Pnt
- symbol: gp_Dir
- symbol: gp_Vec
- symbol: gp_Ax2
- symbol: STEPControl_Reader
- symbol: STEPControl_Writer
- symbol: STEPControl_StepModelType
- symbol: IFSelect_ReturnStatus
- symbol: Interface_Static
emccFlags:
- -O3
- -msimd128
- -sWASM_BIGINT
- -sEVAL_CTORS=2
- -sMODULARIZE=1
- -sEXPORT_ES6=1
- -sENVIRONMENT=web,worker,node
- -sALLOW_MEMORY_GROWTH=1
```
Link it with the step-4 invocation above. On a warm cache, expect a
1.5–3 MB binary in `./output/step_roundtrip.wasm`.
## Variations [#variations]
### Pin the image by digest [#pin-the-image-by-digest]
The `:single-threaded` tag is mutable — a new release rolls forward at any
time. For reproducible CI, replace the tag with an immutable manifest-list
digest:
```bash
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
ghcr.io/taucad/opencascade.js@sha256: \
link my-config.yml
```
See [Reproducible CI](/docs/toolchain/guides/reproducible-ci) for the full pinning recipe.
### Drop into the image for interactive exploration [#drop-into-the-image-for-interactive-exploration]
To browse `build-configs/*.yml` presets, the OCCT source tree, or the
toolchain layout, override the entrypoint:
```bash
docker run --rm -it \
-v "$(pwd):/src" \
--entrypoint bash \
ghcr.io/taucad/opencascade.js:single-threaded
```
The image's working directory is `/opencascade.js/`. `build-configs/`,
`deps/OCCT/`, and the precompiled object cache under `dist/libs/` all
live there.
### When trimming feeds back into your design [#when-trimming-feeds-back-into-your-design]
A trimmed binary makes your application's OCCT dependency graph explicit.
Adding a class to recover from a link error is a signal: either the class
is genuinely part of your call graph, or your code reaches into an OCCT
subsystem it doesn't need. The second case is worth investigating.
## Related [#related]
* [Quickstart — Docker](/docs/toolchain/getting-started/quick-start-docker) — end-to-end first build.
* [Docker image](/docs/toolchain/reference/docker-image) — tags, mounts, OCI labels, provenance.
* [YAML schema](/docs/toolchain/reference/yaml-schema) — every YAML key and its semantics.
* [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) — tuning link-time flags.
* [Reproducible CI](/docs/toolchain/guides/reproducible-ci) — pinning by digest for repeatable builds.
* [Bindgen pipeline](/docs/toolchain/concepts/bindgen-pipeline) — why the trim works without
computing a transitive closure.
---
# build-wasm.sh CLI
URL: /docs/toolchain/reference/cli-build-wasm
`build-wasm.sh` is the orchestration entry point for the OCJS build pipeline.
Each subcommand maps to a stage of the bindgen → compile → link sequence.
## Synopsis [#synopsis]
```bash
./build-wasm.sh [options] []
```
## Subcommands [#subcommands]
### `validate` [#validate]
```bash
./build-wasm.sh validate build-configs/my-config.yml
```
Parses the YAML against `src/customBuildSchema.py` and fails non-zero on any
malformed entry, unknown key, duplicated symbol, or missing
`additionalCppFiles` path. Does **not** run the C++ pipeline.
### `bindings` [#bindings]
```bash
./build-wasm.sh bindings
```
Runs the libclang-driven bindgen against the OCCT headers, emitting one
`.hxx` per class under `build/bindings////` plus
sidecar `.d.ts.json` shards.
Cache: re-uses prior output when header contents and bindgen-filter
fingerprint match. Force regeneration with `--force`.
### `pch` [#pch]
```bash
./build-wasm.sh pch
```
Builds the precompiled header used by every bindings TU. Always passes
`-Xclang -fno-pch-timestamp` so Nx/Docker cache restores don't invalidate the
PCH on disk-mtime drift.
### `link` [#link]
```bash
./build-wasm.sh link build-configs/my-config.yml
```
Compiles every `.hxx` referenced by the YAML's `bindings:` list (cached `.o`
files reused), links them against the precompiled OCCT objects under
`dist/libs/`, and emits the final wasm + JS + `.d.ts` + manifest sibling to
the YAML.
### `clean` [#clean]
```bash
./build-wasm.sh clean # rm build/, cache/, dist/
./build-wasm.sh clean --cache # rm cache/ only
./build-wasm.sh clean --dist # rm dist/ only
```
## Exit codes [#exit-codes]
| Code | Meaning |
| ---- | ------------------------------------------------------- |
| 0 | Success |
| 1 | YAML validation failure |
| 2 | bindgen failure (libclang error) |
| 3 | Compile failure |
| 4 | Link failure (undefined symbol or unresolved reference) |
| 5 | Missing input file |
| 64 | Invalid usage (unknown subcommand or flag) |
## Common flags [#common-flags]
| Flag | Effect |
| ----------------- | ------------------------------------------------------------------------------------------------------ |
| `--config ` | Override `OCJS_CONFIG` for this run — selects a named preset from `build-configs/configurations.json`. |
| `--force` | Bypass cache; rebuild from scratch |
| `--verbose` | Equivalent to `OCJS_VERBOSE=1` |
| `--jobs N` | Set `OCJS_PARALLEL_JOBS=N` |
## Typical CI invocation [#typical-ci-invocation]
```bash
./build-wasm.sh validate build-configs/my-config.yml
./build-wasm.sh link --jobs 8 build-configs/my-config.yml
sha256sum dist/my-config.wasm
```
## Related [#related]
* [Bindgen pipeline](/docs/toolchain/concepts/bindgen-pipeline) — stage diagram and artifact layout.
* [Environment variables](/docs/toolchain/reference/env-vars) — env var catalogue.
* [YAML schema](/docs/toolchain/reference/yaml-schema) — the YAML the CLI consumes.
---
# Named compile-time configurations
URL: /docs/toolchain/reference/configurations
Compile-time presets live in [`build-configs/configurations.json`](https://github.com/taucad/opencascade.js/blob/main/build-configs/configurations.json). Each entry is a flat map of `OCJS_*` environment-variable names to values; the build CLI loads the entry and exports each key before driving `emcc` and `wasm-opt`.
All shipped presets ship with the same *feature* set — native WASM exceptions (`OCJS_EXCEPTIONS=1`, `OCJS_EH_MODE=wasm`), `EVAL_CTORS=2`, and the Closure Compiler on. They differ on optimisation level, threading mode, and the wasm-opt budget.
## Shipped presets [#shipped-presets]
| Preset | Use case | Differentiating flags |
| -------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `single-threaded` | Default `@taucad/opencascade.js` build (`opencascade_full.*`). Browser default. | `OCJS_OPT=-O3`, `OCJS_WASM_OPT_LEVEL=-O4`, `THREADING=single-threaded` |
| `single-threaded-smallest` | Size-tuned variant — smaller binary at a \~5–10% runtime cost. | `OCJS_OPT=-Os`, `OCJS_WASM_OPT_LEVEL=-O3`, `THREADING=single-threaded` |
| `multi-threaded` | Published `@taucad/opencascade.js/multi` build. SAB/COOP+COEP-isolated deployments. | `OCJS_OPT=-O3`, `OCJS_WASM_OPT_LEVEL=-O4`, `THREADING=multi-threaded` |
| `debug` | Fastest build for local iteration. Not for production — no SIMD, no converge, no BigInt. | `OCJS_OPT=-O0`, `OCJS_WASM_OPT_LEVEL=-O0`, `OCJS_SIMD=0`, `OCJS_CONVERGE=false` |
For the full `OCJS_*` matrix that each preset sets — including the flags they share — see [BUILD\_SYSTEM.md](https://github.com/taucad/opencascade.js/blob/main/BUILD_SYSTEM.md#configurationsjson).
## Selecting a preset [#selecting-a-preset]
`build-wasm.sh` reads the `OCJS_CONFIG` env var or the `--config ` CLI flag (the CLI flag wins if both are set). When neither is set, the script falls back to `single-threaded`:
```bash
OCJS_CONFIG=debug ./build-wasm.sh link build-configs/my-config.yml
```
Or via the CLI flag:
```bash
./build-wasm.sh --config debug link build-configs/my-config.yml
```
## Adding a custom preset [#adding-a-custom-preset]
Append a new entry to `build-configs/configurations.json`. The shape is a flat `OCJS_*` env-var map — same keys the shipped presets use:
```json
{
"single-threaded-no-simd": {
"OCJS_OPT": "-Os",
"OCJS_LTO": "0",
"OCJS_EXCEPTIONS": "1",
"OCJS_EH_MODE": "wasm",
"OCJS_SIMD": "0",
"THREADING": "single-threaded",
"OCJS_DEFINES": "OCCT_NO_DUMP",
"OCJS_UNDEFINES": "OCC_CONVERT_SIGNALS",
"OCJS_WASM_OPT_LEVEL": "-O3",
"OCJS_CLOSURE": "true",
"OCJS_EVAL_CTORS": "true",
"OCJS_EVAL_CTORS_LEVEL": "2",
"OCJS_CONVERGE": "true",
"OCJS_BIGINT": "1",
"OCJS_MALLOC": "mimalloc",
"BINARYEN_EXTRA_PASSES": ""
}
}
```
Custom presets share the same compile-`.o` cache as shipped ones — the cache is keyed by the compile-flag fingerprint, not by the preset name. Two presets with identical compile-time flags share cache entries automatically.
## Cache invalidation [#cache-invalidation]
Changing any compile flag invalidates the cached `.o` files that depended on it. The build manifest records the active fingerprint:
```bash
cat build/build-flags.json
```
The published npm tarball ships `dist/opencascade_full.provenance.json` and
`dist/opencascade_full_multi.provenance.json` — same preset / flag information
for each variant, with the commit SHA the wasm was built from. CI scripts should
diff these files across builds to detect surprise cache turnover.
## Related [#related]
* [Two-channel config model](/docs/toolchain/concepts/two-channel-config-model) — why compile-time and link-time config are separate channels.
* [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) — link-time `emccFlags` rationale.
* [Environment variables](/docs/toolchain/reference/env-vars) — every `OCJS_*` env var.
---
# Docker image
URL: /docs/toolchain/reference/docker-image
The maintainer-distributed Docker image lets consumers run custom-trimmed
wasm builds without setting up emsdk, libclang, and Python locally.
## Pulling [#pulling]
```bash
docker pull ghcr.io/taucad/opencascade.js:single-threaded
```
## Tags [#tags]
| Tag | Points to |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `:single-threaded` | Latest release, single-threaded warm cache (default for browser CAD UIs) |
| `:multi-threaded` | Latest release, multi-threaded warm cache (requires COOP/COEP on consumer pages) |
| `:bindgen-base` | Latest release, post-PCH/generate but pre-compile (custom-bindings starting point) |
| `:{{version}}-single-threaded`
`:{{version}}-multi-threaded` | Pinned release (e.g. `:3.0.0-single-threaded`); manifest list of `linux/amd64+arm64` |
| `:{{version}}-bindgen-base` | Pinned release, bindgen-base |
| `:branch-` / `:branch--` | Branch tip, single-threaded, **`linux/amd64`-only**, 7-day GHCR retention |
| `:multi-threaded-branch-` / `…-` | Branch tip, multi-threaded, amd64-only, 7-day retention |
| `:bindgen-base-branch-` / `…-` | Branch tip, bindgen-base, amd64-only, 7-day retention |
| `@sha256:` | Immutable pin — use in CI |
**Pin by digest in production.** The bare-name tags (`:single-threaded`,
`:multi-threaded`, `:bindgen-base`) are mutable and roll forward with every
release. See [Reproducible CI](/docs/toolchain/guides/reproducible-ci) for the pinning
workflow.
The legacy `:beta`, `:rolling`, and `:latest` tags are **not published** by
this fork. Use a version-pinned tag (e.g. `:3.0.0-single-threaded`) or the
manifest-list digest for explicit version control.
## Entrypoint [#entrypoint]
```bash
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
ghcr.io/taucad/opencascade.js:single-threaded \
link mybuild.yml
```
The entrypoint dispatches subcommands through `npx nx run ocjs:` so
runs benefit from Nx's content-addressed cache:
| Subcommand | What it does |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `link ` | End-to-end build. Nx walks `apply-patches → pch → generate → compile-bindings → compile-sources → link` with cache reuse — fresh container = full build, cached re-run = link only. |
| `compile-bindings`, `compile-sources`, `pch`, … | Run an individual Nx target |
| `validate ` | Validate YAML without building |
| `nx ` | Pass-through to `npx nx` (escape hatch) |
Outputs land in `/src` next to your YAML (`OCJS_OUTPUT_DIR=/src` default).
### Override the entrypoint [#override-the-entrypoint]
```bash
docker run --rm -it -v "$(pwd):/src" --entrypoint bash \
ghcr.io/taucad/opencascade.js:single-threaded
```
…drops you into a shell with `emsdk`, libclang, and Python on the PATH.
## Multi-arch matrix [#multi-arch-matrix]
| Tag family | Manifest list | Built on |
| ------------ | ----------------------------- | ----------------------------------------------------------------------------------- |
| Release tags | `linux/amd64` + `linux/arm64` | `ubuntu-latest` (amd64) + `ubuntu-24.04-arm` (arm64), GitHub Actions native runners |
| Branch tags | `linux/amd64` only | `ubuntu-latest`, GitHub Actions |
Releases ship full manifest lists so Apple Silicon and ARM Linux hosts pull
the native arch transparently. Branch tags are amd64-only by design (R12 in
the [production-readiness blueprint](https://github.com/taucad/opencascade.js/blob/main/docs/research/ocjs-docker-production-readiness-blueprint.md))
— ARM hosts will run under Rosetta / QEMU for branch tags.
## OCI labels [#oci-labels]
Inspect via `docker inspect ghcr.io/taucad/opencascade.js:single-threaded`:
| Label | Purpose |
| -------------------------------------- | ------------------------------------------------------------------------------------ |
| `org.opencontainers.image.title` | Stage-specific title (single-threaded, multi-threaded, …) |
| `org.opencontainers.image.description` | Threading model and consumer prerequisites |
| `org.opencontainers.image.source` | [https://github.com/taucad/opencascade.js](https://github.com/taucad/opencascade.js) |
| `org.opencontainers.image.url` | Same as source |
| `org.opencontainers.image.revision` | Git commit the image was built from |
| `org.opencontainers.image.version` | Semver tag |
| `org.opencontainers.image.licenses` | LGPL-2.1-only |
| `org.opencontainers.image.vendor` | taucad |
## Cosign signatures [#cosign-signatures]
Every published image is signed with [cosign](https://github.com/sigstore/cosign)
via OIDC keyless signing — no rotating private keys, signatures published to
the Sigstore Rekor transparency log. Release tags carry **one signature on
the manifest-list digest** that verifies regardless of which arch the
consumer pulls; branch tags carry **per-arch image-digest signatures**.
```bash
cosign verify ghcr.io/taucad/opencascade.js:single-threaded \
--certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
A successful verification confirms the image was built by the
`taucad/opencascade.js` GitHub Actions `docker.yml` workflow and has not
been tampered with since publication.
## Provenance [#provenance]
```bash
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp 'https://github\.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/taucad/opencascade.js:single-threaded
```
The attestation records the source commit, the workflow, and the runner
that produced the image.
## SBOM [#sbom]
```bash
docker buildx imagetools inspect \
--format '{{ json .SBOM }}' \
ghcr.io/taucad/opencascade.js:single-threaded
```
Diff the SBOM across image digests in CI to flag unexpected dep bumps.
## Image stages [#image-stages]
The [Dockerfile](https://github.com/taucad/opencascade.js/blob/main/Dockerfile)
is multi-stage with five logical stages, three of which are published:
| Stage | Published as | Contents |
| -------------------------- | ------------------ | ------------------------------------------------------------------------------- |
| `deps-base` | *(not published)* | emsdk + apt + Node 24 + uv + Python + OCCT/rapidjson/freetype + LLVM 17 headers |
| `bindgen-base` | `:bindgen-base` | deps + npm ci + patches + PCH + `.d.ts.json` index |
| `compiled-single-threaded` | *(not published)* | bindgen + compiled `.o` files + OCCT `.a` (single-threaded) |
| `compiled-multi-threaded` | *(not published)* | bindgen + compiled `.o` files + OCCT `.a` (multi-threaded) |
| `final-single` | `:single-threaded` | compiled-single + OCI labels + entrypoint |
| `final-multi` | `:multi-threaded` | compiled-multi + OCI labels + entrypoint |
Each stage is independently rebuildable via `docker buildx build --target `. See [Bindgen pipeline](/docs/toolchain/concepts/bindgen-pipeline) for how the
stages compose with the build pipeline.
---
# Environment variables
URL: /docs/toolchain/reference/env-vars
The build system reads these env vars before compile and link. Set them on the
command line, in CI, or in `build-configs/configurations.json`.
## Compile-time flags [#compile-time-flags]
| Variable | Default | Effect |
| ----------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OCJS_EXCEPTIONS` | `1` | Compile every TU with `-fwasm-exceptions`. Mixed builds fail at link. |
| `OCJS_SIMD` | `1` | Compile every TU with `-msimd128` (baseline SIMD). |
| `OCJS_RELAXED_SIMD` | `0` | Additionally emit `-mrelaxed-simd`. Chrome/Firefox-only — breaks Safari. |
| `OCJS_BIGINT` | `1` | Compile with `-sWASM_BIGINT`. Saves \~30 KB JS glue. |
| `OCJS_EVAL_CTORS_LEVEL` | `2` | `-sEVAL_CTORS=N`. Levels: 0 off, 1 safe-side-effect, 2 full. |
| `OCJS_LTO` | `0` | Enable LLVM LTO. Smaller binary, \~3× slower build. |
| `OCJS_STRICT_TYPES` | `0` (warn-only) | When the link-time `.d.ts` post-processor rewrites method signatures to `unknown` (or codegen collects unbound-reference diagnostics), the build always prints a triage summary to stderr. Set `=1` to escalate that condition to a build failure. |
## Build orchestration [#build-orchestration]
| Variable | Default | Effect |
| -------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `OCJS_CONFIG` | `single-threaded` | Named preset from `configurations.json`. Alternative to the `--config ` CLI flag — both end up at the same place. |
| `OCJS_PARALLEL_JOBS` | CPU count | Max parallel compile jobs. |
| `OCJS_CACHE_DIR` | `./cache` | Where to store cached `.o` files and bindgen output. |
| `OCJS_BUILD_DIR` | `./build` | Intermediate build outputs. |
| `OCJS_DIST_DIR` | `./dist` | Final wasm + JS + .d.ts artifacts. |
## Docker-specific [#docker-specific]
| Variable | Default | Effect |
| ------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OCJS_DEPS_VERSION` | from `DEPS.json` | Override the dep pinning (OCCT, freetype, rapidjson). |
| `OCJS_EMSDK_DIR` | `/deps/emsdk` | Location of the emscripten SDK inside the image. |
| `OCJS_PYTHON` | `python3` | Python interpreter for the bindgen. |
| `OCJS_OUTPUT_DIR` | `/src` (in container) / `./dist` (host) | Where the link step writes the final `.wasm`, `.js`, `.d.ts`, and `.build-manifest.json`. In-container default is `/src` so the canonical single-mount Quickstart (`docker run -v "$(pwd):/src" …`) writes outputs next to the YAML automatically. Override to a separate path if you need outputs outside the bind-mount. |
## Diagnostics [#diagnostics]
| Variable | Default | Effect |
| ---------------------- | ------- | ------------------------------------------------------------------------- |
| `OCJS_VERBOSE` | `0` | Print every compile / link command. |
| `OCJS_DUMP_CACHE_KEYS` | `0` | Print the cache-key contents on miss (debug spurious cache invalidation). |
## Worked example [#worked-example]
```bash
# Custom-trimmed Os build for an embedded edge runtime.
# OCJS_STRICT_TYPES=1 escalates the missing-typedef warning to a build
# failure -- recommended in CI; omit it for local builds (warn-only is
# the default).
OCJS_CONFIG=single-threaded-smallest \
OCJS_STRICT_TYPES=1 \
OCJS_PARALLEL_JOBS=8 \
./build-wasm.sh link build-configs/edge-runtime.yml
```
## Related [#related]
* [Named compile-time configurations](/docs/toolchain/reference/configurations) — named preset list.
* [Two-channel config model](/docs/toolchain/concepts/two-channel-config-model) — compile-time vs link-time channels.
---
# YAML schema
URL: /docs/toolchain/reference/yaml-schema
The build YAML controls which OCCT classes get bound, which C++ wrapper code
is injected, and which emscripten linker flags drive the final wasm.
## Top-level shape [#top-level-shape]
```yaml
mainBuild:
name:
bindings:
- symbol:
emccFlags:
-
additionalBindCode: |
EMSCRIPTEN_BINDINGS(custom) { ... }
extraBuilds:
- name:
bindings: [...]
emccFlags: [...]
additionalBindCode: |
...
additionalCppCode: |
typedef opencascade::handle Handle_Geom_Curve;
class MyWrapper { ... };
additionalCppFiles:
- path/to/extra.cpp
generateTypescriptDefinitions: true
```
The canonical Cerberus definition lives in `src/customBuildSchema.py`.
## `mainBuild` [#mainbuild]
The primary wasm artifact produced by the YAML.
### `mainBuild.name` [#mainbuildname]
Output filename without extension. `name: my-occt` produces `my-occt.wasm`,
`my-occt.js`, `my-occt.d.ts`, and `my-occt.build-manifest.json`.
### `mainBuild.bindings` [#mainbuildbindings]
Allowlist of OCCT classes to expose to JS via embind. Only classes listed
here (and their transitive base classes) are accessible at runtime.
```yaml
bindings:
- symbol: BRepPrimAPI_MakeBox
- symbol: TopoDS_Shape
- symbol: gp_Pnt
```
The symbol name must match exactly the C++ class name in the generated
binding `.cpp` files under `build/bindings/`. Base classes are auto-included
if missing from the list.
### `mainBuild.emccFlags` [#mainbuildemccflags]
Emscripten linker flags. See [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags)
for the recommended baseline and per-flag rationale.
### `mainBuild.additionalBindCode` [#mainbuildadditionalbindcode]
Per-build raw `EMSCRIPTEN_BINDINGS(...)` registrations compiled with
`` already included. See [Extend with C++](/docs/toolchain/guides/extend-with-cpp).
## Symbol resolution classes [#symbol-resolution-classes]
Every YAML-requested symbol becomes a linked binding through exactly one of four mechanisms. The post-link `build-manifest.json` (schema `build-manifest-v2`) buckets each requested symbol into one of these categories under `symbols`:
1. **Direct compilation.** `bindings: - symbol: gp_Pnt` causes the generator to emit `build/bindings/gp_Pnt.cpp`, which `compileBindings.py` compiles into `build/compiled-bindings/gp_Pnt.cpp.o`. Detected by `ocjs_bindgen.link.manifest_registry.collect_compiled_symbols`. Reported as `satisfied_by_compiled` (count surfaces as `symbols.compiled`).
2. **NCollection typedef alias.** `bindings: - symbol: TColgp_Array1OfPnt` resolves via the canonical mangled spelling `NCollection_Array1_gp_Pnt`; the linker substitutes the typedef at link time. Mapping lives in `build/ncollection-manifest.json`. Detected by `manifest_registry.load_ncollection_alias_index`. Reported under `symbols.alias_resolved` as `{alias, canonical}` entries.
3. **Embind builtin.** OCJS's `BUILTIN_ADDITIONAL_BIND_CODE` block (`OCJS`, `TopoDS`, `TColStd_IndexedDataMapOfStringString`) registers Embind class wrappers with no `.cpp.o` of their own. Detected by `manifest_registry.builtin_binding_symbols` reading `build/additional-bind-symbols.json`.
4. **Consumer `additionalBindCode`.** YAML's own `mainBuild.additionalBindCode` block undergoes the same Embind pathway. The link stage compiles `BUILTIN_ADDITIONAL_BIND_CODE + consumer additionalBindCode` into ONE translation unit so the AST producer emits a single `additional-bind-symbols.json` manifest covering both sources. Reported under `symbols.builtin` (no separate bucket).
Anything that survives all four lookups lands in `symbols.missing` and triggers `validation_passed=false`. The link step also raises immediately via `yaml_build.verifyBindings` (no env-var gate) so a YAML asking for a symbol the toolchain cannot provide fails the link, not just the post-link audit.
Auto-discovered NCollection canonicals (entries the YAML never named directly, but that became reachable from the YAML's scope) are tracked separately in `.provenance.json::nCollectionManifest.{linked, total, dropped}` (schema `wasm-build-provenance-v1.1`). They never appear in `symbols.requested` because they're produced by the discovery pass, not requested by the operator.
## Producer-side manifest contract [#producer-side-manifest-contract]
Every mechanism above has exactly one **producer** — a pipeline stage with the semantic knowledge to compute it — that writes a JSON manifest in `build/` or the dist sidecar. Every downstream **consumer** (link-time `verifyBindings`, post-link `validate-build.py`, `generate-docs.mjs`, `docker-e2e-validate.sh`) reads the manifest through the corresponding `manifest_registry` loader. No consumer re-parses C++, runs regex against source, or re-derives set-difference math.
| Manifest | Producer | Consumer loader |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `build/ncollection-manifest.json` | `ocjs_bindgen.discover` | `manifest_registry.load_ncollection_alias_index` |
| `build/additional-bind-symbols.json` | `runBuild::getAdditionalBindCodeO()` (libclang AST via `ocjs_bindgen.ast.parse_additional_bind_code` + `ocjs_bindgen.ast.walker.extract_class_registrations`) | `manifest_registry.builtin_binding_symbols` |
| `build/compiled-bindings/*.cpp.o` | `compileBindings.py` | `manifest_registry.collect_compiled_symbols` |
| `build/compiled-bindings/binding-report.json` | `compileBindings.py` | `validate-build.py::validate_binding_report` |
| `.provenance.json::nCollectionManifest` | `yaml_build.main` via `provenance.add_linking(ncollection_linked=, ncollection_total=, ncollection_dropped=)` | `generate-docs.mjs`, `scripts/docker-e2e-validate.sh` |
| `build/any-type-report.json` | `generate.py` | `validate-build.py::merge_any_reasons` |
When a manifest is missing, consumers fail loudly with a pointer at `pnpm nx run ocjs:build`. Stale artifacts are stale by definition; rendering them with degraded math produces docs whose numbers contradict the build that produced them.
## `extraBuilds` [#extrabuilds]
Same schema as `mainBuild`. Each entry produces a sibling wasm artifact.
Useful for shipping multiple variants (e.g. baseline + relaxed-SIMD) from one
YAML pass.
## `additionalCppCode` [#additionalcppcode]
Inline C++ injected into a generated `.cpp` file compiled with the same flags
as the bindings TU. Common uses:
1. Handle typedefs (`typedef opencascade::handle Handle_T;`).
2. Free-function helpers reachable from `additionalBindCode`.
3. Small `value_object` POD definitions.
## `additionalCppFiles` [#additionalcppfiles]
List of `.cpp` files concatenated onto `additionalCppCode` before custom-binding
generation runs.
```yaml
additionalCppCode: |
typedef opencascade::handle Handle_Geom_BSplineSurface;
additionalCppFiles:
- wrappers/fair-curve.cpp
- wrappers/shape-cast.cpp
```
* Paths resolve relative to the YAML file's directory; absolute paths also accepted.
* File contents are appended in declaration order.
* `additionalCppCode` wins on conflict because it precedes file contents in the TU.
* A missing file fails-loud at validate time.
## `generateTypescriptDefinitions` [#generatetypescriptdefinitions]
Default `true`. Set to `false` to skip `.d.ts` generation (rare — useful only
for ultra-fast iteration builds).
## Validation [#validation]
```bash
./build-wasm.sh validate build-configs/my-config.yml
```
`validate` checks the YAML against the schema and exits non-zero on any
malformed entry, unknown key, duplicated symbol, or missing
`additionalCppFiles` path. It does not run the C++ pipeline — pair it with
`./build-wasm.sh link` to actually produce the wasm.
## Related [#related]
* [Trim symbols](/docs/toolchain/guides/trim-symbols) — end-to-end workflow.
* [Extend with C++](/docs/toolchain/guides/extend-with-cpp) — when to reach for which mechanism.
* [Custom emcc flags](/docs/toolchain/guides/custom-emcc-flags) — flag-by-flag rationale.
---
# Exception classes
URL: /docs/package/reference/ocjs-package-api/exception-classes
OCCT throws subclasses of `Standard_Failure`. Inside the wasm module, every
throw becomes a `WebAssembly.Exception` instance whose payload is the C++
object. The OCJS bindings expose:
* The exception class hierarchy via bound classes (`oc.Standard_Failure`,
`oc.Standard_OutOfRange`, etc.).
* `getExceptionMessage(error)` — a helper that decodes a `WebAssembly.Exception`
into the OCCT failure text.
## Hierarchy [#hierarchy]
```text
Standard_Failure
├── Standard_OutOfRange
├── Standard_NullObject
├── Standard_NoSuchObject
├── Standard_TypeMismatch
├── Standard_DomainError
├── Standard_DivideByZero
└── Standard_ProgramError
└── Standard_NotImplemented
```
Every subclass surfaces a `GetMessageString()` and inherits the `What()`
method returning a static identifier.
## `getExceptionMessage(error)` [#getexceptionmessageerror]
Named export of `@taucad/opencascade.js`. Accepts a `WebAssembly.Exception`
and returns a string.
```typescript
import init, { getExceptionMessage } from '@taucad/opencascade.js';
try {
riskyOcctCall();
} catch (error: unknown) {
if (error instanceof WebAssembly.Exception) {
console.error('OCCT failure:', getExceptionMessage(error));
return;
}
throw error;
}
```
## What about JS-side `Error.message`? [#what-about-js-side-errormessage]
`WebAssembly.Exception.message` is empty for C++-thrown values. The real
message lives in C++ memory and must be reached through the helper above.
## Related [#related]
* [Debugging WASM exceptions](/docs/package/guides/debugging-wasm-exceptions) — narrative
debugging guide with common failure modes and chrome wasm debugger setup.
---
# init() function
URL: /docs/package/reference/ocjs-package-api/init-function
`init` is the default export of `@taucad/opencascade.js`. It instantiates the
WASM module and returns a Promise resolving to the bound OCCT runtime.
```typescript
import init from '@taucad/opencascade.js';
import wasmUrl from '@taucad/opencascade.js/wasm?url';
const oc = await init({ locateFile: () => wasmUrl });
```
The `@taucad/opencascade.js/wasm` subpath export resolves to the shipped
`opencascade_full.wasm`. The multi-threaded variant uses the same `init`
signature from `@taucad/opencascade.js/multi` with wasm at
`@taucad/opencascade.js/multi/wasm`. See [Bundler & locateFile](/docs/package/guides/bundler-locatefile)
for Vite, Next.js, Bun, Node, and Deno-specific recipes (including the MT variant).
## Options [#options]
**`InitOpenCascadeOptions`** — Options accepted by `init()` from `@taucad/opencascade.js`. Passed straight through to emscripten's `Module` factory.
- **`locateFile`** (`(file: string) => string`, required) — Resolve the URL where the runtime will fetch a sibling asset (most importantly `opencascade_full.wasm`). Mandatory for the V3 single-file build — the runtime no longer auto-discovers its wasm sibling.
The canonical pattern is to point at the `@taucad/opencascade.js/wasm` subpath export and return its URL verbatim.
Tags: @example `() => wasmUrl // import wasmUrl from '@taucad/opencascade.js/wasm?url'`
- **`wasmBinary`** (`ArrayBuffer | Uint8Array | undefined`, optional, default `undefined`) — Override the wasm binary bytes directly. Use this when you've fetched the wasm via a custom transport (e.g. an OPFS cache) and want to skip the runtime's own `fetch(locateFile(...))`.
- **`wasmMemory`** (`WebAssembly.Memory | undefined`, optional, default `undefined`) — Pre-allocated memory for the wasm instance. Most consumers leave this unset and let `ALLOW_MEMORY_GROWTH` size the heap on demand.
- **`print`** (`((text: string) => void) | undefined`, optional, default `console.log`) — Print stdout (e.g. `printf` from custom C++) to a sink.
- **`printErr`** (`((text: string) => void) | undefined`, optional, default `console.error`) — Print stderr (e.g. OCCT diagnostic messages) to a sink.
## Memoised singleton pattern [#memoised-singleton-pattern]
`init` is expensive — wasm instantiation, runtime bootstrap, and class
registration sum to \~200 ms warm / \~2 s cold. Always memoise the Promise.
```typescript
let ocPromise: ReturnType | undefined;
export const getOc = () => (ocPromise ??= init({ locateFile: () => wasmUrl }));
```
Calling `getOc()` from multiple call sites returns the same Promise; only the
first call triggers init.
## Return type [#return-type]
The resolved value is the bound OCCT module — a plain object whose properties
are the bound classes and namespaces (`oc.BRepPrimAPI_MakeBox`,
`oc.TopoDS`, `oc.Interface_Static`) plus emscripten runtime
methods (`oc.FS`, `oc.HEAP8`, `oc.HEAPF32`).
The full surface is documented under the [API Reference](/docs/package/api).
## Multi-threaded subpath [#multi-threaded-subpath]
Import `@taucad/opencascade.js/multi` with wasm at `@taucad/opencascade.js/multi/wasm`. The `init` signature is identical; Emscripten pre-spawns pthread workers during instantiation (`PTHREAD_POOL_SIZE=navigator.hardwareConcurrency` is baked into the binary). No extra JS worker wiring is required beyond `locateFile`.
After `await init(...)`, run this **once** before any parallel-aware OCCT call:
```typescript
oc.BOPAlgo_Options.SetParallelMode(true);
oc.BRepMesh_IncrementalMesh.SetParallelDefault(true);
const pool = oc.OSD_ThreadPool.DefaultPool(-1);
pool.SetNbDefaultThreadsToLaunch(pool.NbThreads());
```
Skipping the pool sizing leaves OCCT's lazy default pool smaller than the pre-spawned worker count and caps speedup on mesh/boolean workloads. See [Multi-threaded build](/docs/package/guides/multi-threading#global-activation--call-once-at-startup) for the full activation walkthrough and [BENCHMARKS.md](https://github.com/taucad/opencascade.js/blob/main/BENCHMARKS.md) for ST vs MT numbers.
---
# Module shape
URL: /docs/package/reference/ocjs-package-api/module-shape
The object returned from `init()` carries every bound class plus emscripten
runtime helpers.
## Bound classes and namespaces [#bound-classes-and-namespaces]
Direct properties named after OCCT classes:
```typescript
const oc = await init({ locateFile });
const box = new oc.BRepPrimAPI_MakeBox(10, 10, 10);
const point = new oc.gp_Pnt(0, 0, 0);
```
OCCT namespaces (e.g. `TopoDS`, `Interface_Static`, `XCAFDoc_DocumentTool`)
appear as static-method-only objects:
```typescript
oc.Interface_Static.SetIVal('write.step.schema', 5);
const edge = oc.TopoDS.Edge(genericShape);
const shapeTool = oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main()).get();
```
## Filesystem (`oc.FS`) [#filesystem-ocfs]
The emscripten MEMFS bridge. Used to write OCCT output (`STEP`, `GLB`, `IGES`)
to an in-memory path, then read the bytes out.
```typescript
const path = `/out_${Date.now()}.step`;
writer.Write(path);
const bytes = oc.FS.readFile(path) as Uint8Array;
oc.FS.unlink(path);
```
| Method | Purpose |
| --------------------------- | -------------------------------------------------------------------------------- |
| `FS.readFile(path)` | Returns bytes as `Uint8Array` (view into wasm memory — copy out before `unlink`) |
| `FS.writeFile(path, bytes)` | Write input file (e.g. for `STEPControl_Reader.ReadFile`) |
| `FS.unlink(path)` | Free the MEMFS path |
| `FS.mkdir(path)` | Create directory (rare — OCCT writes to `/` by default) |
| `FS.readdir(path)` | List directory contents |
Full FS API: [emscripten FS reference](https://emscripten.org/docs/api_reference/Filesystem-API.html).
## Heap views [#heap-views]
| Property | Type |
| ----------------------- | ------------------------------- |
| `oc.HEAP8` | `Int8Array` view of wasm memory |
| `oc.HEAPU8` | `Uint8Array` |
| `oc.HEAP16`, `HEAPU16` | 16-bit views |
| `oc.HEAP32`, `HEAPU32` | 32-bit views |
| `oc.HEAPF32`, `HEAPF64` | Float views |
Use for advanced interop (e.g. reading bytes at a `Standard_Address` pointer
returned from a low-level OCCT API). Most consumers never need these.
## Exception helpers [#exception-helpers]
```typescript
import init, { getExceptionMessage } from '@taucad/opencascade.js';
// see /docs/package/guides/debugging-wasm-exceptions
```
## Module re-init [#module-re-init]
Calling `init()` a second time **re-instantiates** the wasm module —
expensive, and creates two parallel C++ heaps that don't share state. Always
memoise behind a singleton; see [init function](/docs/package/reference/ocjs-package-api/init-function).
---
# API Reference
The full bound OCCT API (5 000+ classes) is exposed as one synthesised Markdown page per package at `/docs/package/api///.mdx`. Use the search endpoint at `/api/search?query=…` to discover entries.