Skip to content

Commit 64abdbd

Browse files
committed
graalvm native image example
1 parent af204ea commit 64abdbd

6 files changed

Lines changed: 228 additions & 50 deletions

File tree

.github/workflows/build-native.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Build GraalVM Native Image
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
build:
8+
runs-on: ubuntu-latest
9+
container:
10+
# reduces the glibc version linked against vs `ubuntu-latest`
11+
# i.e. if you build on ubuntu 24, the binary will _not_ work on ubuntu 22
12+
# but if you build on ubuntu 22, then it will still work on ubuntu 24
13+
image: catthehacker/ubuntu:act-22.04
14+
steps:
15+
- name: Check out repository
16+
uses: actions/checkout@v6
17+
with:
18+
persist-credentials: false
19+
20+
- name: Setup GraalVM
21+
uses: graalvm/setup-graalvm@v1
22+
with:
23+
java-version: '25'
24+
distribution: 'graalvm'
25+
github-token: ${{ secrets.GITHUB_TOKEN }}
26+
27+
- name: Echo Versions and Paths
28+
run: |
29+
echo "GRAALVM_HOME: $GRAALVM_HOME"
30+
echo "JAVA_HOME: $JAVA_HOME"
31+
java --version
32+
native-image --version
33+
34+
- name: Elevate wrapper permissions
35+
run: chmod +x ./gradlew
36+
37+
- name: Setup Gradle
38+
uses: gradle/actions/setup-gradle@v6
39+
with:
40+
dependency-graph: generate-and-submit
41+
cache-read-only: false
42+
43+
- name: Build Linux Native
44+
run: ./gradlew nativeCompile -x test
45+
46+
- name: Elevate binary permissions
47+
run: chmod +x build/native/nativeCompile/*
48+
49+
- name: Get Plugin Name
50+
run: echo "PLUGIN_NAME=$(grep '^plugin_name=' gradle.properties | cut -d'=' -f2)" >> $GITHUB_ENV
51+
52+
- name: Zip Binary And Libs
53+
run: zip -j ${{ env.PLUGIN_NAME }}.zip build/native/nativeCompile/*
54+
55+
- name: Upload Linux Artifact
56+
uses: actions/upload-artifact@v7
57+
with:
58+
path: ${{ env.PLUGIN_NAME }}.zip
59+
if-no-files-found: error
60+
archive: false

README.md

Lines changed: 35 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,61 @@
1-
# ZenithProxy Example Plugin
1+
# ZenithProxy Example Plugin - GraalVM Native Image
22

3-
[ZenithProxy](https://github.com/rfresh2/ZenithProxy) is a Minecraft proxy and bot.
3+
This branch contains an example of how to build a native image of ZenithProxy with your plugin included
44

5-
This repository is an example core plugin for ZenithProxy, allowing you to add custom modules and commands.
5+
See GraalVM documentation for more information: https://www.graalvm.org/latest/reference-manual/native-image/basics/
66

7-
## Installing Plugins
7+
This is _not_ compiling a GraalVM native image of your plugin alone - it is the full ZenithProxy application with your plugin included.
88

9-
Plugins are only supported on the `java` ZenithProxy release channel (i.e. not `linux`).
9+
GraalVM native image is not able to dynamically load more plugins at runtime.
1010

11-
Place plugin jars in the `plugins` folder inside the same folder as the ZenithProxy launcher.
11+
# Usage
1212

13-
Restart ZenithProxy to load plugins. Loading plugins after launch or hot reloading is not supported.
13+
`./gradlew nativeCompile`
1414

15-
## Creating Plugins
15+
The executable will be at: `build/native/nativeCompile/`
1616

17-
Use this repository as a template to create your own plugin repository.
17+
# Licensing Implications
1818

19-
### Plugin Structure
19+
ZenithProxy is licensed under the [AGPL](https://www.gnu.org/licenses/agpl-3.0.en.html)
2020

21-
Each plugin needs a main class that implements `ZenithProxyPlugin` and is annotated with `@Plugin`.
21+
If you distribute a native image of ZenithProxy with your plugin, this - including your plugin - must also be licensed under the AGPL
2222

23-
Plugin metadata like its unique id, version, and supported MC versions is defined in the `@Plugin` annotation.
23+
This is different than if you were to only distribute your plugin jar, as it now is a full ZenithProxy application.
2424

25-
[See example](https://github.com/rfresh2/ZenithProxyExamplePlugin/blob/1.21.4/src/main/java/org/example/ExamplePlugin.java)
25+
# Limitations
2626

27-
### Plugin API
27+
### Reachability Metadata
2828

29-
The `ZenithProxyPlugin` interface requires you to implement an `onLoad` method.
29+
A GraalVM `Feature` class is provided to quickly register additional reflection
3030

31-
This method provides a `PluginAPI` object that you can use to register modules, commands, and config files.
31+
You may need to register other types of reachability metadata yourself: https://www.graalvm.org/latest/reference-manual/native-image/metadata/
3232

33-
`Module` and `Command` classes are implemented the same as in the ZenithProxy source code.
33+
### Launcher
3434

35-
I recommend looking at existing modules, commands, and plugins for examples.
35+
The compiled build output is a full ZenithProxy application with the plugin included.
3636

37-
* [Module](https://github.com/rfresh2/ZenithProxy/tree/1.21.4/src/main/java/com/zenith/module)
38-
* [Command](https://github.com/rfresh2/ZenithProxy/tree/1.21.4/src/main/java/com/zenith/command)
39-
* Plugins
40-
* [ZenithProxyVillagerTrader](https://github.com/rfresh2/ZenithProxyVillagerTrader)
41-
* [ZenithProxyWebAPI](https://github.com/rfresh2/ZenithProxyWebAPI)
42-
* [ZenithProxyChatControl](https://github.com/rfresh2/ZenithProxyChatControl)
43-
* More in [my discord server](https://discord.com/channels/1127460556710883391/1369081651564515358)
37+
So the launcher will not be downloading and running this application during normal operation.
4438

45-
### JavaDocs
39+
You can execute the application directly from the command line
4640

47-
https://maven.2b2t.vc/javadoc/releases/com/zenith/ZenithProxy/1.21.4-SNAPSHOT
41+
or the launcher will execute the application if you replace the normal `linux` release channel exe at: `launcher/ZenithProxy` but this is not officially supported.
4842

49-
### Building Plugins
43+
### Multiple Plugins
5044

51-
Execute the Gradle `build` task: `./gradlew build` - or double-click the task in Intellij
45+
This example only builds ZenithProxy with this single plugin
5246

53-
The built plugin jar will be in the `build/libs` directory.
47+
Any plugin included on the compile classpath will be included in the native image, so in theory it may be possible
5448

55-
### Testing Plugins
49+
but no example or additional documentation is provided here
5650

57-
Execute the `run` task: `./gradlew run` - or double-click the task in Intellij
51+
# New GraalVM Plugin Checklist
5852

59-
This will run ZenithProxy with your plugin loaded in the `run` directory.
60-
61-
### New Plugin Checklist
62-
63-
1. Edit `gradle.properties`:
64-
- `plugin_name` - Name of your plugin, shown to users and in the plugin jar file name (e.g. `ExamplePlugin`)
65-
- `plugin_id` - Unique identifier for your plugin (e.g. `example-plugin`)
66-
- Must start with a lowercase letter and contain only lowercase letters, numbers, or dashes (`-`)
67-
- `mc` - MC version of ZenithProxy your plugin is compiled for (e.g. `1.21.4`)
68-
- `maven_group` - Java package for your project (e.g. `com.github.rfresh2`)
69-
1. Move files to your new corresponding package / maven group:
70-
- Example: `src/main/java/org/example` -> `src/main/java/com/github/rfresh2`
71-
- First create the new package in `src/main/java`. Then click and drag original subpackages/classes to your new one
72-
- Do this with Intellij to avoid manually editing all the source files
73-
- You must also create and move package folders for the `src/main/templates` folder
74-
1. Edit `ExamplePlugin.java`, or remove it and create a new main class
75-
- Make sure to update the `@Plugin` annotation
53+
1. Edit `ExamplePluginReflectionFeature.java`:
54+
- Move the feature class to your new corresponding package / maven group
55+
- Enter any additional packages to register reflection for (if needed)
56+
2. Edit `src/resources/META-INF/native-image/org.example/exampleplugin/native-image.properties`
57+
- Edit the `Feature` class path to match your new feature class
58+
- Create directories and move this file to your corresponding package/maven group
59+
3. Build and verify
60+
- Any additional build arguments needed should be set in `build.gradle.kts`'s `graalvmNative` section
61+
- e.g. config for plugin dependencies, different build options, etc.

build.gradle.kts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
plugins {
22
id("zenithproxy.plugin.dev") version "1.0.0-SNAPSHOT"
3+
id("org.graalvm.buildtools.native") version "1.1.0"
34
}
45

56
group = properties["maven_group"] as String
67
version = properties["plugin_version"] as String
78
val mc = properties["mc"] as String
89
val pluginId = properties["plugin_id"] as String
10+
val pluginName = properties["plugin_name"] as String
911

10-
java { toolchain { languageVersion = JavaLanguageVersion.of(25) } }
12+
java {
13+
toolchain {
14+
languageVersion = JavaLanguageVersion.of(25)
15+
nativeImageCapable = true
16+
}
17+
}
1118

1219
zenithProxyPlugin {
1320
templateProperties = mapOf(
@@ -33,6 +40,8 @@ repositories {
3340
dependencies {
3441
zenithProxy("com.zenith:ZenithProxy:$mc-SNAPSHOT")
3542

43+
compileOnly("org.graalvm.sdk:nativeimage:25.0.3")
44+
3645
/** to include dependencies into your plugin jar **/
3746
// shade("com.github.ben-manes.caffeine:caffeine:3.2.0")
3847
}
@@ -57,4 +66,46 @@ tasks {
5766
// exclude(dependency(":jspecify:.*"))
5867
// }
5968
}
69+
nativeCompile {
70+
notCompatibleWithConfigurationCache("not compatible with configuration cache")
71+
dependsOn(shadowJar, build)
72+
}
73+
generateResourcesConfigFile {
74+
notCompatibleWithConfigurationCache("not compatible with configuration cache")
75+
dependsOn(shadowJar)
76+
}
77+
}
78+
79+
graalvmNative {
80+
binaries {
81+
named("main") {
82+
javaLauncher = javaToolchains.launcherFor {
83+
languageVersion.set(JavaLanguageVersion.of(25))
84+
nativeImageCapable = true
85+
}
86+
imageName = pluginName
87+
mainClass = "com.zenith.Proxy"
88+
quickBuild = false // set to true for fast builds while developing
89+
verbose = true
90+
sharedLibrary = false
91+
buildArgs.addAll(
92+
// required - otherwise plugin classes will be stripped
93+
// you may need to add more if your plugin has dependencies
94+
"-H:Preserve=package=${project.group}.*",
95+
"-O3", // highest optimization level, but slowest build times
96+
"-H:DeadlockWatchdogInterval=30",
97+
"-H:+CompactingOldGen",
98+
"-H:+TrackPrimitiveValues",
99+
"-H:+TreatAllTypeReachableConditionsAsTypeReached",
100+
"-H:+UsePredicates",
101+
"--future-defaults=all",
102+
"-R:MaxHeapSize=200m",
103+
"-march=x86-64-v3",
104+
"--gc=serial",
105+
"-J-XX:MaxRAMPercentage=90"
106+
)
107+
configurationFileDirectories.from(file("src/main/resources/META-INF/native-image"))
108+
}
109+
metadataRepository { enabled = true }
110+
}
60111
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package graalvm.org.example;
2+
3+
import org.example.BuildConstants;
4+
import org.graalvm.nativeimage.hosted.Feature;
5+
import org.graalvm.nativeimage.hosted.RuntimeReflection;
6+
7+
import java.io.IOException;
8+
import java.net.URLDecoder;
9+
import java.util.HashSet;
10+
import java.util.List;
11+
import java.util.Set;
12+
import java.util.jar.JarFile;
13+
14+
public class ExamplePluginReflectionFeature implements Feature {
15+
16+
@Override
17+
public void beforeAnalysis(BeforeAnalysisAccess access) {
18+
try {
19+
final List<String> prefixes = List.of(
20+
BuildConstants.MAVEN_GROUP
21+
);
22+
var cl = access.getApplicationClassLoader();
23+
for (String prefix : prefixes) {
24+
var classNames = scanClasses(cl, prefix);
25+
for (var className : classNames) {
26+
var clazz = cl.loadClass(className);
27+
access.registerAsUsed(clazz);
28+
registerReflection(clazz);
29+
}
30+
}
31+
} catch (Exception e) {
32+
throw new RuntimeException(e);
33+
}
34+
}
35+
36+
static void registerReflection(Class<?> clazz) {
37+
RuntimeReflection.register(clazz);
38+
RuntimeReflection.register(clazz.getDeclaredConstructors());
39+
RuntimeReflection.register(clazz.getDeclaredMethods());
40+
RuntimeReflection.register(clazz.getDeclaredFields());
41+
if (clazz.isRecord()) {
42+
var recordComponents = clazz.getRecordComponents();
43+
if (recordComponents != null) {
44+
for (var comp : recordComponents) {
45+
var accessor = comp.getAccessor();
46+
RuntimeReflection.register(accessor);
47+
}
48+
}
49+
}
50+
}
51+
52+
private static Set<String> scanClasses(ClassLoader classLoader, String pkg) throws IOException {
53+
var classes = new HashSet<String>();
54+
var packageName = pkg.replace(".", "/");
55+
var resources = classLoader.getResources(packageName);
56+
while (resources.hasMoreElements()) {
57+
var packageURL = resources.nextElement();
58+
if (!packageURL.getProtocol().equals("jar")) continue;
59+
var jarFileName = URLDecoder.decode(packageURL.getFile(), "UTF-8");
60+
jarFileName = jarFileName.substring(5, jarFileName.indexOf("!"));
61+
try (var jf = new JarFile(jarFileName)) {
62+
var jarEntries = jf.entries();
63+
while (jarEntries.hasMoreElements()) {
64+
var entryName = jarEntries.nextElement().getName();
65+
if (entryName.startsWith(packageName) && entryName.endsWith(".class")) {
66+
var clazzName = entryName
67+
// back to class-loadable format
68+
.replace("/", ".")
69+
// chop off .class suffix
70+
.substring(0, entryName.lastIndexOf('.'));
71+
classes.add(clazzName);
72+
}
73+
}
74+
}
75+
}
76+
return classes;
77+
}
78+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Args=--features=graalvm.org.example.ExamplePluginReflectionFeature

src/main/templates/org/example/BuildConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ public class BuildConstants {
88
public static final String MC_VERSION = "${mc_version}";
99

1010
public static final String PLUGIN_ID = "${plugin_id}";
11+
12+
public static final String MAVEN_GROUP = "${maven_group}";
1113
}

0 commit comments

Comments
 (0)