inlay

August 10, 2026 · View on GitHub

Fetch and verify WebAssembly modules from OCI registries for the JVM.

Quick start

<build>
  <plugins>
    <plugin>
      <groupId>io.roastedroot</groupId>
      <artifactId>inlay-maven-plugin</artifactId>
      <version>${inlay.version}</version>
      <executions>
        <execution>
          <goals><goal>fetch</goal></goals>
          <configuration>
            <modules>
              <module>
                <imageRef>ghcr.io/roastedroot/sqlite4j-wasm:3.51.0</imageRef>
                <outputFile>${project.build.directory}/wasm/libsqlite3.wasm</outputFile>
              </module>
            </modules>
          </configuration>
        </execution>
      </executions>
    </plugin>

    <plugin>
      <groupId>run.endive</groupId>
      <artifactId>endive-compiler-maven-plugin</artifactId>
      <version>${endive.version}</version>
      <executions>
        <execution>
          <goals><goal>compile</goal></goals>
          <configuration>
            <wasmFile>${project.build.directory}/wasm/libsqlite3.wasm</wasmFile>
            <name>com.example.SqliteModule</name>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

See examples/basic/.

Caching

Fetched artifacts are cached in ~/.cache/inlay/ (or $XDG_CACHE_HOME/inlay/), keyed by OCI digest. Survives mvn clean.

FlagBehavior
(default)Cache hit → no network
-Dinlay.updateRe-resolves digest, updates lock file
-Dinlay.noCacheAlways pulls from registry
-Dinlay.skipSkips fetch entirely

Lock file

inlay:fetch writes wkg.lock using the wkg format (Rust wasm-pkg-common types compiled to wasm). Commit it for reproducible builds.

If the registry digest changes but the lock hasn't been updated, the build fails.

Upgrading

mvn io.roastedroot:inlay-maven-plugin:fetch -Dinlay.update

Local development

When iterating on a wasm module locally, you can avoid re-fetching from the registry:

Option A — outputFile outside target/: set outputFile to a path that mvn clean won't delete. Once the lock entry exists and the file is on disk, the plugin skips fetching:

<module>
  <imageRef>ghcr.io/roastedroot/my-module-wasm:1.0.0</imageRef>
  <outputFile>${project.basedir}/src/main/resources/wasm/my-module.wasm</outputFile>
</module>

Rebuild your wasm locally and overwrite that file — the plugin will not touch it.

Option B — skip the fetch entirely:

mvn compile -Dinlay.skip

Useful when you manage the wasm file yourself and don't need the plugin at all during local builds.

Authentication

Resolved in order:

  1. Maven settings.xml <server> entries keyed by registry hostname
  2. Docker/Podman credential stores (~/.docker/config.json)

For local dev: oras login ghcr.io or docker login ghcr.io. For CI:

<server>
  <id>ghcr.io</id>
  <username>${env.GHCR_USER}</username>
  <password>${env.GHCR_TOKEN}</password>
</server>

Package references

Use wkg-style names instead of full OCI refs. Namespaces resolve via ~/.config/wasm-pkg/config.toml:

<module>
  <packageRef>roastedroot:sqlite4j-wasm@3.51.0</packageRef>
</module>

Builtins: wasi:*wasi.dev, ba:*bytecodealliance.org.

Signature verification

Verification runs inline after fetch — no separate step. Configure on each module:

<module>
  <imageRef>ghcr.io/roastedroot/sqlite4j-wasm:3.51.0</imageRef>
  <outputFile>${project.build.directory}/wasm/libsqlite3.wasm</outputFile>
  <sigstoreIssuer>https://token.actions.githubusercontent.com</sigstoreIssuer>
  <sigstoreIdentity>https://github.com/roastedroot/*</sigstoreIdentity>
</module>

Uses sigstore-java keyless verification. Expects a .sigstore.json bundle alongside the artifact.

Publishing wasm to OCI

Requires oras CLI and optionally cosign. Packages appear at https://github.com/orgs/<org>/packagesset visibility to public after first push.

echo $GHCR_TOKEN | oras login ghcr.io -u $GHCR_USER --password-stdin

oras push ghcr.io/roastedroot/sqlite4j-wasm:3.51.0 \
  libsqlite3.wasm:application/wasm

cosign sign --yes ghcr.io/roastedroot/sqlite4j-wasm:3.51.0

GitHub Actions — publisher

Use the reusable workflow included in this repo:

name: Publish Wasm
on:
  push:
    paths: ['wasm-build/**']
    branches: [main]
permissions:
  contents: read
  packages: write
  id-token: write
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./wasm-build/build.sh
  publish:
    needs: build
    uses: roastedroot/inlay/.github/workflows/wasm-publish.yml@main
    with:
      wasm-file: wasm-build/output/my-module.wasm
      image-ref: ghcr.io/roastedroot/my-module-wasm
      version: '1.0.0'

GitHub Actions — consumer

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: 'maven'
      - run: mvn verify

No wasm build step, no Docker, no Rust. inlay:fetch pulls from GHCR during generate-sources.

Building from source

Requires JDK 17+ and Rust with wasm32-wasip1 target. Docker for integration tests.

cd wkg-wasm && make build
mvn install                  # unit tests
mvn verify                   # unit + integration tests (Docker required)

See CONTRIBUTING.md for architecture and development details.