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 first.
- People should run your build without installing Java. Build and test the image, then package it.
- Code that works on the JVM fails in the native image. Regenerate reachability metadata for it.
- The build fails with a certificate error behind a corporate proxy. See 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. Use a prebuilt runtime for those tasks. Start with Extending Vis 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-graalvminstalls and selects it. The build rejects Oracle GraalVM and mismatched versions before startingnative-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_HEAPcan bound the builder heap, for example12g. 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:
- Add your code to the relevant domain under
src/com/blockether/vis/internal/and any required library todeps.edn. Java libraries must be on the build classpath. Adding a JAR beside an already-built binary does not load new code. - 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. - If you add a module initializer, include its qualified registration symbol in
resources/META-INF/vis/manifest.edn's ordered:initializationvector, after its dependencies. This manifest registers built-in modules and supplies the native build's entry points. It is not the Python extension loader. - Add tests under
test/for registration and actual calls, including failure cases. Run the affected JVM tests withclojure -M:test --namespace your.test-ns. Add matching execution coverage undertest-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. 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:
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:
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:
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:
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.
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-timeflags are needed. - The manifest's initialization vector is the native root set.
build.cljderives 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:+ForeignAPISupportand--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:
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 — add Python tools without rebuilding the engine.
- Runtime distributions — use a prebuilt runtime when you do not change JVM capabilities.
- Running a gateway — supervise and secure your custom engine.
- Python SDK — call your capability through an owned engine or gateway.
