Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ git -C "$REPO" config core.hooksPath .githooks

bash "$REPO/scripts/fetch-wasm-deps.sh"

# The compiled engine is not committed (KOF-186), so a fresh clone has none —
# install the published one up front rather than making the first `bun run dev`
# wait for it. --optional: a session branched off an unmerged engine change has no
# release to fetch and must build instead, which is not a reason to fail startup.
bash "$REPO/scripts/fetch-wasm-engine.sh" --optional

# clang-tidy >= 20 for scripts/clang-tidy.sh (the C++ engine linter, mirroring
# the DeepSource rules — see .clang-tidy). The sandbox's default clang-tidy-18
# cannot parse the emsdk clang-22-era headers.
Expand Down
130 changes: 118 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,36 @@ jobs:
- name: clang-tidy
run: bash scripts/clang-tidy.sh

# The compiled engine is not committed (KOF-186) — it is published as the
# release `engine-<id>` and pulled in by scripts/fetch-wasm-engine.sh. The ID is
# a content hash of the engine sources, computed here on the runner rather than
# in the `wasm` job: that job runs inside the dependency container, which may
# have no git metadata to hash.
engine-id:
name: Engine build ID
runs-on: ubuntu-24.04
outputs:
id: ${{ steps.compute.outputs.id }}

steps:
- uses: actions/checkout@v4

- id: compute
run: |
ID="$(bash scripts/engine-version.sh)"
echo "id=$ID" >> "$GITHUB_OUTPUT"
echo "Engine build ID: $ID"

wasm:
name: WASM (Emscripten)
needs: [engine-id]
runs-on: ubuntu-24.04
container:
image: ghcr.io/mkofler96/kofem-dependencies:0.0.2

env:
KOFEM_ENGINE_ID: ${{ needs.engine-id.outputs.id }}

steps:
- uses: actions/checkout@v4

Expand All @@ -86,36 +110,112 @@ jobs:
path: |
web/src/wasm/pkg/kofem_wasm_emcc.js
web/src/wasm/pkg/kofem_wasm_emcc.wasm
# deps tag is part of the key so bumping the toolchain image invalidates
# any artifact built with the previous emscripten/Binaryen pair.
key: wasm-${{ runner.os }}-deps0.0.2-${{ hashFiles('engine/**', 'scripts/build-wasm.sh') }}
web/src/wasm/pkg/.engine-id
key: wasm-${{ runner.os }}-${{ needs.engine-id.outputs.id }}

- name: WASM cache status
# On a cache miss the engine may still have been built before — every push
# to main publishes one. Downloading it beats a 20-minute rebuild, and it
# exercises the exact path contributors use.
- name: Try the published engine release
if: steps.wasm-cache.outputs.cache-hit != 'true'
run: bash scripts/fetch-wasm-engine.sh --optional

- name: Engine source status
id: engine
run: |
if [ "${{ steps.wasm-cache.outputs.cache-hit }}" = "true" ]; then
echo "WASM cache HIT — skipping build"
if [ -s web/src/wasm/pkg/kofem_wasm_emcc.wasm ]; then
echo "have=true" >> "$GITHUB_OUTPUT"
echo "Engine ${KOFEM_ENGINE_ID} obtained without building."
else
echo "WASM cache MISS — full build will run"
echo "have=false" >> "$GITHUB_OUTPUT"
echo "Engine ${KOFEM_ENGINE_ID} not published yet — full build will run."
fi

- name: Clean stale CMake cache
if: steps.wasm-cache.outputs.cache-hit != 'true'
if: steps.engine.outputs.have != 'true'
run: |
rm -f target/wasm-build/CMakeCache.txt
rm -rf target/wasm-build/CMakeFiles

- name: Build WASM engine
if: steps.wasm-cache.outputs.cache-hit != 'true'
if: steps.engine.outputs.have != 'true'
run: bash scripts/build-wasm.sh

# include-hidden-files carries the .engine-id stamp through to the jobs that
# download this, so they can tell which sources the binary came from.
- name: Upload WASM artifacts
uses: actions/upload-artifact@v4
with:
name: kofem-wasm-pkg
retention-days: 7
include-hidden-files: true
path: |
web/src/wasm/pkg/kofem_wasm_emcc.js
web/src/wasm/pkg/kofem_wasm_emcc.wasm
web/src/wasm/pkg/.engine-id

# Publish the engine so contributors, the Docker build and future CI runs can
# download it instead of committing a ~34 MB blob per engine change (KOF-186).
# Only from main: a release is a public, immutable artifact, and PR branches are
# served by the workflow artifact above.
publish-engine:
name: Publish engine release
needs: [engine-id, wasm]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04

permissions:
contents: write

steps:
- uses: actions/checkout@v4

- name: Download WASM artifacts
uses: actions/download-artifact@v4
with:
name: kofem-wasm-pkg
path: engine-dist/

# Releases are content-addressed, so an existing tag already holds a binary
# built from these exact sources — re-uploading would only churn it.
- name: Publish release if absent
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.engine-id.outputs.id }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already published — nothing to do."
exit 0
fi

cd engine-dist
sha256sum kofem_wasm_emcc.js kofem_wasm_emcc.wasm > SHA256SUMS

# Marked as a prerelease so these build artifacts never take over the
# "Latest release" banner from an actual product release.
if gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "WASM engine $TAG" \
--prerelease \
--notes "Compiled KoFEM WASM engine, built from \`engine/\` at ${GITHUB_SHA}.

Installed automatically by \`scripts/fetch-wasm-engine.sh\`, which resolves this
tag from the engine sources in your checkout — you should not need to download
these files by hand." \
kofem_wasm_emcc.js kofem_wasm_emcc.wasm SHA256SUMS; then
exit 0
fi

# Two pushes carrying the same engine sources can race here. Losing the
# race is fine — the winner published a binary built from those same
# sources. Anything else is a real failure.
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG was published concurrently — nothing to do."
exit 0
fi
echo "ERROR: publishing $TAG failed and no release exists." >&2
exit 1

frontend:
name: Frontend
Expand All @@ -126,11 +226,17 @@ jobs:
contents: read
packages: write

env:
# This job takes the engine from the `wasm` job's artifact, so the
# package.json pre-hooks must not try to fetch a release on top of it —
# a PR that changes engine/ has no published release yet.
KOFEM_WASM_SKIP_FETCH: 1

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
# Full history + tags so `git describe --tags` can stamp the image.
# Full history + tags so `git describe` can stamp the image.
fetch-depth: 0

- name: Download WASM artifacts
Expand Down Expand Up @@ -242,7 +348,7 @@ jobs:
# image never ships. Reuses the gha cache, so the push build below is warm.
- name: Smoke-test production image
run: |
VERSION=$(git describe --tags --abbrev=0 || echo 'smoke')
VERSION=$(git describe --tags --abbrev=0 --match 'v*' || echo 'smoke')
docker buildx build \
--load \
--platform linux/amd64 \
Expand Down Expand Up @@ -276,7 +382,7 @@ jobs:
run: |
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
IMAGE_PREFIX="ghcr.io/${OWNER_LC}"
VERSION=$(git describe --tags --abbrev=0 || echo 'no-tags-found')
VERSION=$(git describe --tags --abbrev=0 --match 'v*' || echo 'no-tags-found')
IMAGE_TAG="${VERSION}-$(git rev-parse --short HEAD)"

if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ web/node_modules/
web/dist/
web/.vite/

# Compiled WASM engine — a ~34 MB build output, published as a GitHub Release
# instead of committed (KOF-186). Install it with scripts/fetch-wasm-engine.sh.
# The thin adapter beside it (kofem_wasm.js, kofem_wasm.d.ts) is hand-written and
# stays tracked.
web/src/wasm/pkg/kofem_wasm_emcc.js
web/src/wasm/pkg/kofem_wasm_emcc.wasm
web/src/wasm/pkg/.engine-id

# macOS
.DS_Store

Expand Down
49 changes: 39 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ KoFEM/
├── examples/ # Validation cases, shell-coupling scripts, web examples
├── test_files/ # STEP/IGES fixtures used by tests and examples
└── scripts/
├── build-wasm.sh # CMake/Emscripten WASM build
├── docker-build-wasm.sh # Docker wrapper (Mac / CI)
├── fetch-wasm-deps.sh # Pull the precompiled OCCT/Netgen/MFEM WASM libs
├── clang-tidy.sh # C++ lint, mirrors the DeepSource PR gate
├── build-wasm.sh # CMake/Emscripten WASM build
├── docker-build-wasm.sh # Docker wrapper (Mac / CI)
├── fetch-wasm-deps.sh # Pull the precompiled OCCT/Netgen/MFEM WASM libs
├── fetch-wasm-engine.sh # Pull the compiled engine from its GitHub Release
├── engine-version.sh # Content hash of the engine sources → release tag
├── clang-tidy.sh # C++ lint, mirrors the DeepSource PR gate
├── test-bc-validation.sh
└── test-shell.sh
```
Expand All @@ -51,12 +53,33 @@ OCCT / Netgen / MFEM (.a, compiled with emcc)
engine/cpp/engine.cpp (C++17, calls libs directly, Embind API)
↓ emcmake cmake + ninja
kofem_wasm_emcc.js + kofem_wasm_emcc.wasm
published as the release engine-<id>, fetched into web/src/wasm/pkg/
web/src/wasm/pkg/kofem_wasm.js (thin adapter, committed)
solver.worker.ts (awaits init(), calls methods on the KofemModule instance)
```

### The compiled engine is not in git

`kofem_wasm_emcc.js` + `.wasm` are ~34 MB of build output. Committing them added a
fresh full-size blob to history on every engine change, so they are gitignored and
published as a GitHub Release instead (KOF-186).

`scripts/engine-version.sh` hashes the engine sources (`engine/`,
`scripts/build-wasm.sh`, `scripts/fetch-wasm-deps.sh`) into an ID; CI publishes each
main build as the release `engine-<id>`; `scripts/fetch-wasm-engine.sh` resolves the
ID from the checkout and downloads the matching binary. Nothing to bump by hand, and
changed sources can never resolve to a stale binary.

What this means in practice:

- `bun run dev|build|test` fetch the engine first via package.json pre-hooks. A
matching `.engine-id` stamp in `web/src/wasm/pkg/` makes that a no-op.
- **After changing `engine/cpp`, build locally** (`scripts/docker-build-wasm.sh`):
no release exists for unmerged sources. The build writes the stamp, so the fetch
then leaves your binary alone. The release appears once the PR lands on `main`.
- Do not re-add the binaries to git, and do not `git add -f` them.

### Shells and multibody

Not every model is a bag of tetrahedra. Two features cut across the pipeline and
Expand Down Expand Up @@ -105,8 +128,13 @@ git config core.hooksPath .githooks
cargo check
cargo test

# Build the WASM engine. Needs Emscripten plus the precompiled OCCT/Netgen/MFEM
# WASM libs; scripts/fetch-wasm-deps.sh pulls them, or use the Docker wrapper.
# Install the prebuilt engine from its release. Needs no toolchain at all, and the
# bun scripts below run it for you.
./scripts/fetch-wasm-engine.sh

# Build the WASM engine from source instead — required after editing engine/cpp.
# Needs Emscripten plus the precompiled OCCT/Netgen/MFEM WASM libs;
# scripts/fetch-wasm-deps.sh pulls them, or use the Docker wrapper.
./scripts/build-wasm.sh # or: ./scripts/docker-build-wasm.sh

# Install and run the web frontend (uses bun, not npm)
Expand All @@ -118,9 +146,10 @@ bash scripts/clang-tidy.sh
cd web && bun run typecheck && bun run lint && bun run format:check && bun run test
```

Building the WASM engine rewrites the committed `web/src/wasm/pkg/*.wasm`
(~34 MB). Only rebuild when you have changed C++ sources — an unnecessary rebuild
adds tens of megabytes of history to a repository whose `.git` is already ~80 MB.
Building the WASM engine rewrites `web/src/wasm/pkg/*.wasm` (~34 MB). Those files
are gitignored — see [The compiled engine is not in git](#the-compiled-engine-is-not-in-git)
— so a rebuild no longer costs history, but it does cost ~20 minutes. Only rebuild
when you have changed C++ sources; otherwise let the fetch script install it.

## Geometry vs. Mesh — Critical Terminology

Expand Down
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ For more examples visit [KoFEM Examples](https://kofem.org/examples/)
## Run it with Docker

The app is a static frontend (pre-built WASM engine + React UI) served by Nginx.
The compiled WASM engine is committed under `web/src/wasm/pkg/`, so **you don't
need Emscripten, Rust, or the C++ libraries — just Docker.** The container
listens on port **10000**.
The compiled WASM engine is downloaded as a prebuilt binary, so **you don't need
Emscripten, Rust, or the C++ libraries — just Docker.** The container listens on
port **10000**.

Option A — Pull the published image (recommended)

Expand All @@ -35,23 +35,30 @@ docker run ghcr.io/mkofler96/kofem-web:latest
Option B — Build it yourself

```bash
# Fetch the prebuilt engine into web/src/wasm/pkg/ (see Development below).
bash scripts/fetch-wasm-engine.sh

# Build context is the web/ directory (Dockerfile lives at web/Dockerfile).
docker build -t kofem-web ./web
docker run kofem-web
```

## Development

To rebuild the WASM engine from C++ source first, run
`bash scripts/docker-build-wasm.sh` — it compiles the engine inside a Docker
container and regenerates `web/src/wasm/pkg/`. The committed engine is already
up to date, so this is only needed if you change the C++ sources.
Afterwards, the web frontend can be run by

```bash
cd web && bun install && bun run dev
```

That is all you need — the compiled engine (~34 MB) is a build output, so it is
published as a GitHub Release rather than committed, and `bun run dev`, `build`
and `test` download the one matching your checkout via
`scripts/fetch-wasm-engine.sh`. You can also run that script directly.

To change the engine you do need the C++ toolchain. `bash scripts/docker-build-wasm.sh`
compiles it inside a Docker container and writes `web/src/wasm/pkg/` itself; from
then on the fetch step sees your local build and leaves it alone. CI publishes the
release for your sources once they land on `main`.

> [!NOTE]
> The wasm docker build is layered on top of [KoFEM-Dependencies](https://github.com/mkofler96/KoFEM-Dependencies), which contains the precompiled wasm OCCT, Netgen and MFEM libraries. KoFEM can be compiled without docker using the script `scripts/build-wasm.sh`, but then the OCCT, Netgen and MFEM source code must be downloaded and will be compiled during the KoFEM compilation. This will take some time.

Expand Down
13 changes: 13 additions & 0 deletions scripts/build-wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ mkdir -p "$OUT_DIR"
cp "$BUILD_DIR/kofem_wasm_emcc.js" "$OUT_DIR/kofem_wasm_emcc.js"
cp "$BUILD_DIR/kofem_wasm_emcc.wasm" "$OUT_DIR/kofem_wasm_emcc.wasm"

# Record which sources these binaries came from, so scripts/fetch-wasm-engine.sh
# leaves a fresh local build alone instead of replacing it with a download. The
# build may run somewhere without git metadata (a CI container checked out from a
# tarball), in which case the caller passes the ID down; with neither, drop the
# stamp rather than leave a stale one claiming the wrong sources.
ENGINE_ID="${KOFEM_ENGINE_ID:-$(bash "$REPO_ROOT/scripts/engine-version.sh" 2>/dev/null || true)}"
if [ -n "$ENGINE_ID" ]; then
printf '%s\n' "$ENGINE_ID" >"$OUT_DIR/.engine-id"
else
rm -f "$OUT_DIR/.engine-id"
echo "NOTE: engine ID unavailable here — wrote no .engine-id stamp."
fi

echo "Done."
echo " $OUT_DIR/kofem_wasm_emcc.js"
echo " $OUT_DIR/kofem_wasm_emcc.wasm"
5 changes: 5 additions & 0 deletions scripts/docker-build-wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,15 @@ echo ""
echo "==> Launching build container..."
echo ""

# Computed on the host: the ID is derived from git metadata, and git inside the
# container would refuse the bind-mounted repo as dubiously owned anyway.
ENGINE_ID="$(bash "${REPO_ROOT}/scripts/engine-version.sh")"

docker run --rm \
--platform "${PLATFORM}" \
-v "${REPO_ROOT}:/repo" \
-w /repo \
-e KOFEM_ENGINE_ID="${ENGINE_ID}" \
"${IMAGE}" \
bash -c "
rm -f target/wasm-build/CMakeCache.txt
Expand Down
Loading
Loading