# Native builds for Java and Clojure extensions

This guide shows how to add **Java or Clojure capabilities inside Vis** and compile
them into a GraalVM native image that runs without a JVM. This is an in-tree
engine build, not a drop-in JAR plugin system.

## When to use

- **You are adding Java or Clojure code that must run inside the Vis engine.** [Add
  and test it on the JVM](#add-and-test-your-jvm-capability) first.
- **People should run your build without installing Java.** [Build and test the
  image](#build-and-test-the-image), then [package it](#package-and-run-your-build).
- **Code that works on the JVM fails in the native image.** [Regenerate reachability
  metadata](#regenerating-reachability-metadata) for it.
- **The build fails with a certificate error behind a corporate proxy.** See
  [Building behind a corporate TLS proxy](#building-behind-a-corporate-tls-proxy).

You do **not** need a native build to run a gateway, use the Python SDK or add a Python extension.
Java and Clojure programs can call the [HTTP API](http-api.md). Use a [prebuilt runtime](distributions.md)
for those tasks. Start with [Extending Vis](extending.md) for Python tools.

## Prerequisites

- A source checkout, the Clojure CLI and the usual build tools for your platform.
  Run the commands below from the repository root.
- GraalVM Community Edition at the version in `.graalvm-version`.
  `bin/require-graalvm` installs and selects it. The build rejects Oracle
  GraalVM and mismatched versions before starting `native-image`.
- At least 16 GB RAM for the builder JVM, plus room for the operating system,
  dependency cache and build outputs. Build on a larger machine rather than a
  small production VPS. `VIS_NATIVE_BUILDER_HEAP` can bound the builder heap,
  for example `12g`. A smaller heap is not a substitute for enough memory.

Build for the target operating system and architecture. A macOS binary does not
run on Linux. `vis-agent update --track dev` updates managed JVM source. It does
not compile a native image.

## Add and test your JVM capability

Make the capability work on the JVM before compiling a native image:

1. Add your code to the relevant domain under `src/com/blockether/vis/internal/`
   and any required library to `deps.edn`. Java libraries must be on the build
   classpath. Adding a JAR beside an already-built binary does not load new code.
2. Register tool bindings through `com.blockether.vis.internal.extension.core`.
   A Clojure binding can call your Java API. Give each exported tool an explicit
   input/output contract and a human-readable Activity presentation.
3. If you add a module initializer, include its qualified registration symbol in
   `resources/META-INF/vis/manifest.edn`'s ordered `:initialization` vector, after
   its dependencies. This manifest registers built-in modules and supplies the
   native build's entry points. It is not the Python extension loader.
4. Add tests under `test/` for registration and actual calls, including failure
   cases. Run the affected JVM tests with `clojure -M:test --namespace your.test-ns`.
   Add matching execution coverage under `test-native/` for the compiled engine.

Native compilation cannot find classes that code loads dynamically at run time. Check the
reflection, resource and foreign-function needs of your library in [Native-image
configuration](#native-image-configuration). Rebuild each time you change JVM code or dependencies.
Python extensions still load separately.

## Build and test the image

Select the pinned toolchain, then compile:

```bash
eval "$(bin/require-graalvm --export)"
clojure -T:build native
```

The build compiles Clojure ahead of time and then runs GraalVM Native Image. Expect a long build.
When it succeeds, you get:

| Output | Purpose |
| --- | --- |
| `target/vis` | Compiled native engine |
| `target/vis.build` | Version, source revision, release track and build time |
| `target/vis-agent-python/` | Required Python worker, native library and interpreter files |
| `target/vis.jar` | Intermediate JVM build artifact, not a native release bundle |

Run the native suite against the new image:

```bash
clojure -M:test-native
```

To test another built image, set `VIS_NATIVE_BIN` to its path. Keep its matching
embedded Python runtime (`vis-agent-python/`) beside it. These tests execute the binary. Passing JVM tests
or reaching the end of `native-image` does not prove that tools, HTTP or Python
work inside it.

## Package and run your build

Stage the complete engine bundle. This command **replaces**
`target/release-bundle` and writes the named archive. Keep unrelated files out of
that staging directory. Create a fresh test home outside your project:

```bash
bin/stage-release-bundle target/vis target/vis-agent-local.tar.gz
export VIS_TEST_HOME="$(mktemp -d)"
export VIS_HOME="$VIS_TEST_HOME/.vis"
HOME="$VIS_TEST_HOME" target/release-bundle/vis-agent \
  -Duser.home="$VIS_TEST_HOME" --version
```

The staging helper renames `target/vis` to `vis-agent-native` and adds the
`vis-agent` launcher, build stamp, embedded Python runtime
(`vis-agent-python/`) and installer. Ship these
files together. The terminal client is a separate release component. This
engine bundle is enough for gateway and SDK stdio use, not a complete TUI
installation.

`VIS_HOME` keeps the launcher away from your installed Vis. `-Duser.home` moves the engine's own
state, and `HOME` does the same for child processes.

This test home is temporary. Prepare its provider configuration and credentials
before starting a gateway. Use a persistent service-account home for deployment.
Choose an unused port and run:

```bash
unset VIS_GATEWAY_URL VIS_GATEWAY_TOKEN VIS_DB_PATH
HOME="$VIS_TEST_HOME" target/release-bundle/vis-agent \
  -Duser.home="$VIS_TEST_HOME" gateway start \
  --host 127.0.0.1 --port 7890 --require-token
```

Its default state is under `$VIS_TEST_HOME/.vis`. Separate state is not a security
sandbox: the process can still use allowed project files and inherited environment
variables. Do not replace or stop a shared gateway for this test.

For an owned Python job, pass the absolute staged launcher path as
`Agent(executable=...)` or `LocalEngine(executable=...)`. These use a temporary
session database. See the [Python SDK](python-sdk.md).

To use your new capability, connect an SDK client to this custom gateway or wrap
the staged launcher with Agent. The client still needs no native compilation.
Only the engine containing your Java/Clojure code was rebuilt.

## Native-image configuration

The build uses AOT classes and the original dependency jars, not only a flattened uberjar. At build
time, GraalVM finds the `META-INF/native-image/<group>/<artifact>/` directory of each jar. Libraries
supply their own configuration:

- Reflection, resources and FFM downcalls are declared in
  `reachability-metadata.json`.
- Clojure-generated classes are initialized at build time, so no
  per-namespace `--initialize-at-build-time` flags are needed.
- The manifest's initialization vector is the native root set. `build.clj`
  derives entry points from it.
- Native libraries such as fff, rift and imaging are reached through the
  Foreign Function and Memory API. Each binding jar enables it with its own
  `-H:+ForeignAPISupport` and `--enable-native-access`.

The Python runtime files are copied into `target/vis-agent-python/` beside the
binary rather than included in it. CPython loads files from this directory.
Release bundles include it, and `bin/vis-agent` sets `VIS_PYTHON_NATIVE_PATH`
to its location.

## Regenerating reachability metadata

Run the relevant code under the tracing agent
(`-agentlib:native-image-agent=config-merge-dir=…`), then use the repository's
filter to remove Clojure-internal entries. `native_reachability_test` checks
the engine's metadata. Also run `clojure -M:test-native` against the built
binary. Passing JVM tests do not verify native execution.

## Building behind a corporate TLS proxy

A fresh GraalVM trusts only public roots, so a network that intercepts TLS
fails with `SunCertPathBuilderException: unable to find valid certification
path to requested target`. Point the build at the extra root instead of
patching the JDK:

```bash
export VIS_CA_CERT=/etc/ssl/certs/corporate-ca.pem
eval "$(bin/require-graalvm --export)"
clojure -T:build native
```

`bin/require-graalvm` imports the PEM into a copy of the JDK's `cacerts` under
`${XDG_CACHE_HOME:-~/.cache}/vis`. It gives that copy to `curl`. It also exports the copy through
`JAVA_TOOL_OPTIONS`, so every forked JVM and the `native-image` builder use it.
`bin/require-graalvm --truststore` prints the path. To use an existing keystore, set
`VIS_TRUSTSTORE=/path/store.p12` with `VIS_TRUSTSTORE_PASSWORD` and `VIS_TRUSTSTORE_TYPE`. Their
defaults are `changeit` and `PKCS12`.

## See also

- [Extending Vis](extending.md) — add Python tools without rebuilding the engine.
- [Runtime distributions](distributions.md) — use a prebuilt runtime when you do not change JVM capabilities.
- [Running a gateway](gateway-service.md) — supervise and secure your custom engine.
- [Python SDK](python-sdk.md) — call your capability through an owned engine or gateway.
