From 66c648b5dbfc8d02ef3cbddf80c249e2f1c01afe Mon Sep 17 00:00:00 2001 From: andreatp Date: Fri, 24 Jul 2026 12:47:22 +0100 Subject: [PATCH 01/20] Extend Maven plugin with redline native compilation support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture: - Bridge module uses exec:java with Generator.main() instead of the Maven plugin, breaking the circular dependency (plugin → redline → bridge → plugin) - New redline/build-time-compiler module with RedlineGenerator that handles native code generation and extends generated sources with builder()/safeBuilder()/loadNativeCode() methods - Maven plugin calls RedlineGenerator when is configured Config extended with: - redlineTargets: list of target triples for cross-compilation - targetResourceFolder: where .native files are written Generated module class gains (when redlineTargets configured): - loadNativeCode(): loads platform-specific native code from resources - builder(): automatic backend selection (native if platform + Java 25 supported, bytecode otherwise) - safeBuilder(): always uses bytecode compiler NativeMachineFactory.Builder gains toInstanceBuilder() to return a configured Instance.Builder for the generated builder() method. RedlineTarget gains fromTriple() for target triple lookup. --- bom/pom.xml | 5 + .../endive/build/time/compiler/Config.java | 45 ++++- .../endive/build/time/compiler/Generator.java | 31 ++++ compiler-maven-plugin/pom.xml | 4 + .../time/maven/EndiveCompilerGenMojo.java | 33 +++- pom.xml | 5 + .../api/internal/RedlineTarget.java | 9 + redline/bridge/pom.xml | 71 +++++++- redline/build-time-compiler/pom.xml | 38 ++++ .../experimental/build/RedlineGenerator.java | 172 ++++++++++++++++++ redline/pom.xml | 2 + 11 files changed, 403 insertions(+), 12 deletions(-) create mode 100644 redline/build-time-compiler/pom.xml create mode 100644 redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java diff --git a/bom/pom.xml b/bom/pom.xml index af3f71462..afe9217ed 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -78,6 +78,11 @@ redline-bridge-experimental ${project.version} + + run.endive + redline-build-time-compiler-experimental + ${project.version} + run.endive redline-compiler-experimental diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java index 1aba2b9b9..22a870d6e 100644 --- a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java @@ -1,6 +1,7 @@ package run.endive.build.time.compiler; import java.nio.file.Path; +import java.util.List; import java.util.Set; import java.util.StringJoiner; import run.endive.compiler.InterpreterFallback; @@ -46,6 +47,16 @@ public final class Config { */ private final String moduleInterface; + /** + * target triples for redline native compilation (empty = no native compilation) + */ + private final List redlineTargets; + + /** + * the target resource folder for native code files + */ + private final Path targetResourceFolder; + private Config( Path wasmFile, String name, @@ -54,7 +65,9 @@ private Config( Path targetWasmFolder, InterpreterFallback interpreterFallback, Set interpretedFunctions, - String moduleInterface) { + String moduleInterface, + List redlineTargets, + Path targetResourceFolder) { this.wasmFile = wasmFile; this.name = name; this.targetClassFolder = targetClassFolder; @@ -63,6 +76,8 @@ private Config( this.interpreterFallback = interpreterFallback; this.interpretedFunctions = interpretedFunctions; this.moduleInterface = moduleInterface; + this.redlineTargets = redlineTargets; + this.targetResourceFolder = targetResourceFolder; } public Path wasmFile() { @@ -97,6 +112,18 @@ public String moduleInterface() { return moduleInterface; } + public List redlineTargets() { + return redlineTargets; + } + + public Path targetResourceFolder() { + return targetResourceFolder; + } + + public boolean hasRedlineTargets() { + return redlineTargets != null && !redlineTargets.isEmpty(); + } + public static Builder builder() { return new Builder(); } @@ -126,6 +153,8 @@ public static final class Builder { private InterpreterFallback interpreterFallback = InterpreterFallback.FAIL; private Set interpretedFunctions; private String moduleInterface; + private List redlineTargets = List.of(); + private Path targetResourceFolder; private Builder() {} @@ -169,6 +198,16 @@ public Builder withModuleInterface(String moduleInterface) { return this; } + public Builder withRedlineTargets(List redlineTargets) { + this.redlineTargets = redlineTargets; + return this; + } + + public Builder withTargetResourceFolder(Path targetResourceFolder) { + this.targetResourceFolder = targetResourceFolder; + return this; + } + public Config build() { return new Config( wasmFile, @@ -178,7 +217,9 @@ public Config build() { targetWasmFolder, interpreterFallback, interpretedFunctions, - moduleInterface); + moduleInterface, + redlineTargets, + targetResourceFolder); } } } diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java index 1e47938c7..624b51c41 100644 --- a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java @@ -57,6 +57,37 @@ public Generator(Config config) { this.config = config; } + public static void main(String[] args) throws IOException { + if (args.length < 5) { + throw new IllegalArgumentException( + "Usage: Generator " + + " " + + " [interpreterFallback] [moduleInterface]"); + } + var configBuilder = + Config.builder() + .withWasmFile(Path.of(args[0])) + .withName(args[1]) + .withTargetClassFolder(Path.of(args[2])) + .withTargetSourceFolder(Path.of(args[3])) + .withTargetWasmFolder(Path.of(args[4])); + if (args.length > 5 && !args[5].isEmpty()) { + configBuilder.withInterpreterFallback( + run.endive.compiler.InterpreterFallback.valueOf(args[5])); + } + if (args.length > 6 && !args[6].isEmpty()) { + configBuilder.withModuleInterface(args[6]); + } + var config = configBuilder.build(); + var generator = new Generator(config); + var interpreted = generator.generateResources(); + generator.generateMetaWasm(interpreted); + generator.generateSources(); + if (config.moduleInterface() != null && !config.moduleInterface().isEmpty()) { + generator.generateModuleInterface(config.moduleInterface()); + } + } + public Set generateResources() throws IOException { var module = Parser.parse(config.wasmFile()); var machineName = config.name() + "Machine"; diff --git a/compiler-maven-plugin/pom.xml b/compiler-maven-plugin/pom.xml index 223b8df00..a7ea02a6a 100644 --- a/compiler-maven-plugin/pom.xml +++ b/compiler-maven-plugin/pom.xml @@ -22,6 +22,10 @@ run.endive compiler + + run.endive + redline-build-time-compiler-experimental + org.apache.maven maven-core diff --git a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java index 5866a32ed..ee1c3a82b 100644 --- a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java +++ b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.util.List; import java.util.Set; import java.util.TreeSet; import org.apache.maven.model.Resource; @@ -14,6 +15,7 @@ import run.endive.build.time.compiler.Config; import run.endive.build.time.compiler.Generator; import run.endive.compiler.InterpreterFallback; +import run.endive.redline.experimental.build.RedlineGenerator; /** * This plugin generates an invokable library from the compiled Wasm @@ -77,6 +79,23 @@ public class EndiveCompilerGenMojo extends AbstractMojo { @Parameter(required = false) String moduleInterface; + /** + * Target triples for Redline native compilation. When set, the plugin + * cross-compiles the Wasm module to native code for each target using + * Cranelift and generates builder()/safeBuilder() methods in the module class. + * Example: x86_64-unknown-linux-gnu, aarch64-apple-darwin + */ + @Parameter(required = false) + List redlineTargets; + + /** + * The target resource folder for native code files (.native). + */ + @Parameter( + required = true, + defaultValue = "${project.build.directory}/generated-resources/endive-compiler") + private File targetResourceFolder; + /** * The current Maven project. */ @@ -87,7 +106,7 @@ public class EndiveCompilerGenMojo extends AbstractMojo { public void execute() throws MojoExecutionException { getLog().info("Compiling classes for " + name + " from " + wasmFile); - var config = + var configBuilder = Config.builder() .withWasmFile(wasmFile.toPath()) .withName(name) @@ -97,7 +116,11 @@ public void execute() throws MojoExecutionException { .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) .withModuleInterface(moduleInterface) - .build(); + .withTargetResourceFolder(targetResourceFolder.toPath()); + if (redlineTargets != null && !redlineTargets.isEmpty()) { + configBuilder.withRedlineTargets(redlineTargets); + } + var config = configBuilder.build(); var generator = new Generator(config); @@ -106,6 +129,12 @@ public void execute() throws MojoExecutionException { generator.generateMetaWasm(finalInterpretedFunctions); generator.generateSources(); + if (config.hasRedlineTargets()) { + var redlineGenerator = new RedlineGenerator(config); + redlineGenerator.generateNativeCode(); + redlineGenerator.extendGeneratedSources(); + } + if (moduleInterface != null && !moduleInterface.isEmpty()) { generator.generateModuleInterface(moduleInterface); } diff --git a/pom.xml b/pom.xml index 58f1441f0..a0f75ffff 100644 --- a/pom.xml +++ b/pom.xml @@ -268,6 +268,11 @@ redline-bridge-experimental ${project.version} + + run.endive + redline-build-time-compiler-experimental + ${project.version} + run.endive redline-compiler-experimental diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java index 933d2feef..dba0b0ab9 100644 --- a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java @@ -27,6 +27,15 @@ public String resourceSuffix() { return resourceSuffix; } + public static Optional fromTriple(String triple) { + for (RedlineTarget target : values()) { + if (target.triple.equals(triple)) { + return Optional.of(target); + } + } + return Optional.empty(); + } + public static Optional detectHost() { String osName = System.getProperty("endive.redline.os.name", System.getProperty("os.name", "")) diff --git a/redline/bridge/pom.xml b/redline/bridge/pom.xml index 412153b93..784cfdd8a 100644 --- a/redline/bridge/pom.xml +++ b/redline/bridge/pom.xml @@ -29,21 +29,76 @@ + - run.endive - endive-compiler-maven-plugin + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} - redline-bridge + add-generated-sources - compile + add-source + generate-sources - run.endive.redline.experimental.bridge.internal.Cranelift - ${project.basedir}/../cranelift_bridge.wasm - WARN - run.endive.redline.experimental.bridge.internal.CraneliftBridge + + ${project.build.directory}/generated-sources/endive-compiler + + + + + add-generated-resources + + add-resource + + generate-resources + + + + ${project.build.directory}/generated-resources/endive-compiler + + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + run.endive + build-time-compiler + ${project.version} + + + + + redline-bridge-compile + + java + + generate-sources + + run.endive.build.time.compiler.Generator + + ${project.basedir}/../cranelift_bridge.wasm + run.endive.redline.experimental.bridge.internal.Cranelift + ${project.build.directory}/generated-resources/endive-compiler + ${project.build.directory}/generated-sources/endive-compiler + ${project.build.directory}/generated-resources/endive-compiler + WARN + run.endive.redline.experimental.bridge.internal.CraneliftBridge + + true + false diff --git a/redline/build-time-compiler/pom.xml b/redline/build-time-compiler/pom.xml new file mode 100644 index 000000000..50ee68428 --- /dev/null +++ b/redline/build-time-compiler/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-build-time-compiler-experimental + jar + Endive - Redline Build Time Compiler + Build-time native code generation via Cranelift + + + + com.github.javaparser + javaparser-core + + + run.endive + build-time-compiler + + + run.endive + redline-api-experimental + + + run.endive + redline-compiler-experimental + + + run.endive + wasm + + + diff --git a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java new file mode 100644 index 000000000..f16ed9840 --- /dev/null +++ b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java @@ -0,0 +1,172 @@ +package run.endive.redline.experimental.build; + +import static com.github.javaparser.StaticJavaParser.parseClassOrInterfaceType; +import static com.github.javaparser.StaticJavaParser.parseType; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.Modifier; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.stmt.ReturnStmt; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import run.endive.build.time.compiler.Config; +import run.endive.redline.experimental.api.NativeCodeSerializer; +import run.endive.redline.experimental.api.internal.RedlineTarget; +import run.endive.redline.experimental.compiler.internal.NativeCompiler; +import run.endive.wasm.Parser; + +public final class RedlineGenerator { + + private final Config config; + + public RedlineGenerator(Config config) { + this.config = config; + } + + public void generateNativeCode() throws IOException { + if (!config.hasRedlineTargets()) { + return; + } + var module = Parser.parse(config.wasmFile()); + var packagePath = config.getPackageName().replace('.', '/'); + var baseName = config.getBaseName(); + var resourceDir = config.targetResourceFolder().resolve(packagePath); + Files.createDirectories(resourceDir); + + for (String triple : config.redlineTargets()) { + byte[][] compiledCode = NativeCompiler.compileAll(triple, module); + + var target = + RedlineTarget.fromTriple(triple) + .orElseThrow( + () -> + new IllegalArgumentException( + "Unknown target triple: " + triple)); + var nativeFile = + resourceDir.resolve(baseName + "." + target.resourceSuffix() + ".native"); + + try (var out = new FileOutputStream(nativeFile.toFile())) { + NativeCodeSerializer.serialize(compiledCode, out); + } + } + } + + public void extendGeneratedSources() throws IOException { + if (!config.hasRedlineTargets()) { + return; + } + var packagePath = config.getPackageName().replace('.', '/'); + var baseName = config.getBaseName(); + var sourceFile = + config.targetSourceFolder().resolve(packagePath).resolve(baseName + ".java"); + + var cu = StaticJavaParser.parse(sourceFile); + var type = cu.getClassByName(baseName).orElseThrow(); + + cu.addImport("run.endive.redline.experimental.api.NativeCodeSerializer"); + cu.addImport("run.endive.redline.experimental.api.internal.RedlineTarget"); + cu.addImport("run.endive.redline.experimental.runner.NativeMachineFactory"); + cu.addImport("java.io.InputStream"); + cu.addImport("java.io.IOException"); + cu.addImport("java.io.UncheckedIOException"); + cu.addImport("run.endive.runtime.Instance"); + + generateNativeCodeHolderInnerClass(type, baseName); + generateLoadNativeCodeMethod(type); + generateBuilderMethod(type, baseName); + generateSafeBuilderMethod(type, baseName); + + Files.writeString(sourceFile, cu.toString()); + } + + private static void generateNativeCodeHolderInnerClass( + ClassOrInterfaceDeclaration type, String moduleName) { + var holderClass = + new ClassOrInterfaceDeclaration( + NodeList.nodeList( + new Modifier(Modifier.Keyword.PRIVATE), + new Modifier(Modifier.Keyword.STATIC)), + false, + "NativeCodeHolder"); + type.addMember(holderClass); + + holderClass.addField( + parseType("byte[][]"), "CODE", Modifier.Keyword.STATIC, Modifier.Keyword.FINAL); + + var initBody = holderClass.addStaticInitializer(); + + initBody.addStatement( + StaticJavaParser.parseStatement( + "var host = RedlineTarget.detectHost().orElse(null);")); + + initBody.addStatement( + StaticJavaParser.parseStatement( + "if (host == null || Runtime.version().feature() < 25) {\n" + + " CODE = null;\n" + + "} else {\n" + + " String resource = \"" + + moduleName + + ".\" + host.resourceSuffix() + \".native\";\n" + + " try (InputStream in = " + + moduleName + + ".class.getResourceAsStream(resource)) {\n" + + " CODE = (in == null) ? null" + + " : NativeCodeSerializer.deserialize(in);\n" + + " } catch (IOException e) {\n" + + " throw new UncheckedIOException(" + + "\"Failed to load native code\", e);\n" + + " }\n" + + "}")); + } + + private static void generateLoadNativeCodeMethod(ClassOrInterfaceDeclaration type) { + var method = + type.addMethod("loadNativeCode", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) + .setType(parseType("byte[][]")); + method.createBody() + .addStatement( + new ReturnStmt( + new FieldAccessExpr(new NameExpr("NativeCodeHolder"), "CODE"))); + } + + private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, String moduleName) { + var method = + type.addMethod("builder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) + .setType(parseClassOrInterfaceType("Instance.Builder")); + + var body = method.createBody(); + body.addStatement(StaticJavaParser.parseStatement("var module = load();")); + body.addStatement( + StaticJavaParser.parseStatement("byte[][] nativeCode = loadNativeCode();")); + body.addStatement( + StaticJavaParser.parseStatement( + "if (nativeCode != null) {\n" + + " return NativeMachineFactory.builder(module)" + + ".withPrecompiledCode(nativeCode)" + + ".toInstanceBuilder();\n" + + "}")); + body.addStatement( + StaticJavaParser.parseStatement( + "return Instance.builder(module).withMachineFactory(" + + moduleName + + "::create);")); + } + + private static void generateSafeBuilderMethod( + ClassOrInterfaceDeclaration type, String moduleName) { + var method = + type.addMethod("safeBuilder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) + .setType(parseClassOrInterfaceType("Instance.Builder")); + + method.createBody() + .addStatement( + StaticJavaParser.parseStatement( + "return Instance.builder(load()).withMachineFactory(" + + moduleName + + "::create);")); + } +} diff --git a/redline/pom.xml b/redline/pom.xml index 13f29061f..e8c0f3c7c 100644 --- a/redline/pom.xml +++ b/redline/pom.xml @@ -17,6 +17,7 @@ api bridge + build-time-compiler compiler runner-jffi runner-jffi-tests @@ -29,6 +30,7 @@ [25,) + it runner runner-tests From fb51e1eebe8878f7c697cb2ad5b888e41409ad64 Mon Sep 17 00:00:00 2001 From: andreatp Date: Fri, 24 Jul 2026 15:04:01 +0100 Subject: [PATCH 02/20] Add true flag and E2E integration test - Add true shortcut to compile for all supported platforms (instead of listing 6 target triples) - Add redline/it module with Maven Invoker integration test: - Compiles add.wat.wasm with native code for all platforms - Tests builder() (native), safeBuilder() (bytecode), loadNativeCode() - Verifies native and bytecode produce identical results - Add logging when redline native compilation runs --- .../endive/experimental/compiler/cli/Cli.java | 19 ++++- .../endive/build/time/compiler/Generator.java | 31 ------- .../build/time/compiler/GeneratorMain.java | 40 +++++++++ .../time/maven/EndiveCompilerGenMojo.java | 48 +++-------- pom.xml | 8 +- redline/bridge/pom.xml | 2 +- .../experimental/build/RedlineGenerator.java | 9 ++ redline/it/pom.xml | 68 ++++++++++++++++ .../it/src/it/redline-e2e/invoker.properties | 1 + redline/it/src/it/redline-e2e/pom.xml | 77 ++++++++++++++++++ .../test/java/endive/test/RedlineE2eTest.java | 53 ++++++++++++ .../src/test/resources/add.wat.wasm | Bin 0 -> 67 bytes redline/it/src/it/settings.xml | 35 ++++++++ 13 files changed, 314 insertions(+), 77 deletions(-) create mode 100644 build-time-compiler/src/main/java/run/endive/build/time/compiler/GeneratorMain.java create mode 100644 redline/it/pom.xml create mode 100644 redline/it/src/it/redline-e2e/invoker.properties create mode 100644 redline/it/src/it/redline-e2e/pom.xml create mode 100644 redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java create mode 100644 redline/it/src/it/redline-e2e/src/test/resources/add.wat.wasm create mode 100644 redline/it/src/it/settings.xml diff --git a/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java b/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java index 9e4245caa..6a954e005 100644 --- a/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java +++ b/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java @@ -71,6 +71,14 @@ public String[] getVersion() { "The indexes of functions that should be interpreted, separated by commas") Set interpretedFunctions; + @CommandLine.Option( + order = 7, + names = "--module-interface", + description = + "Fully qualified class name for which to generate _ModuleExports and" + + " _ModuleImports wrappers") + String moduleInterface; + @Override public void run() { var config = @@ -82,6 +90,7 @@ public void run() { .withTargetWasmFolder(targetWasmFolder) .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) + .withModuleInterface(moduleInterface) .build(); var generator = new Generator(config); @@ -90,13 +99,19 @@ public void run() { var interpretedFunctions = generator.generateResources(); generator.generateMetaWasm(interpretedFunctions); generator.generateSources(); + if (moduleInterface != null && !moduleInterface.isEmpty()) { + generator.generateModuleInterface(moduleInterface); + } } catch (IOException e) { throw new CommandLine.PicocliException("Failed to execute the command", e); } } + public static int execute(String[] args) { + return new CommandLine(new Cli()).execute(args); + } + public static void main(String[] args) { - int exitCode = new CommandLine(new Cli()).execute(args); - System.exit(exitCode); + System.exit(execute(args)); } } diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java index 624b51c41..1e47938c7 100644 --- a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java @@ -57,37 +57,6 @@ public Generator(Config config) { this.config = config; } - public static void main(String[] args) throws IOException { - if (args.length < 5) { - throw new IllegalArgumentException( - "Usage: Generator " - + " " - + " [interpreterFallback] [moduleInterface]"); - } - var configBuilder = - Config.builder() - .withWasmFile(Path.of(args[0])) - .withName(args[1]) - .withTargetClassFolder(Path.of(args[2])) - .withTargetSourceFolder(Path.of(args[3])) - .withTargetWasmFolder(Path.of(args[4])); - if (args.length > 5 && !args[5].isEmpty()) { - configBuilder.withInterpreterFallback( - run.endive.compiler.InterpreterFallback.valueOf(args[5])); - } - if (args.length > 6 && !args[6].isEmpty()) { - configBuilder.withModuleInterface(args[6]); - } - var config = configBuilder.build(); - var generator = new Generator(config); - var interpreted = generator.generateResources(); - generator.generateMetaWasm(interpreted); - generator.generateSources(); - if (config.moduleInterface() != null && !config.moduleInterface().isEmpty()) { - generator.generateModuleInterface(config.moduleInterface()); - } - } - public Set generateResources() throws IOException { var module = Parser.parse(config.wasmFile()); var machineName = config.name() + "Machine"; diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/GeneratorMain.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/GeneratorMain.java new file mode 100644 index 000000000..fa24c8d3d --- /dev/null +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/GeneratorMain.java @@ -0,0 +1,40 @@ +package run.endive.build.time.compiler; + +import java.io.IOException; +import java.nio.file.Path; +import run.endive.compiler.InterpreterFallback; + +public final class GeneratorMain { + + private GeneratorMain() {} + + public static void main(String[] args) throws IOException { + if (args.length < 5) { + throw new IllegalArgumentException( + "Usage: GeneratorMain " + + " " + + " [interpreterFallback] [moduleInterface]"); + } + var configBuilder = + Config.builder() + .withWasmFile(Path.of(args[0])) + .withName(args[1]) + .withTargetClassFolder(Path.of(args[2])) + .withTargetSourceFolder(Path.of(args[3])) + .withTargetWasmFolder(Path.of(args[4])); + if (args.length > 5 && !args[5].isEmpty()) { + configBuilder.withInterpreterFallback(InterpreterFallback.valueOf(args[5])); + } + if (args.length > 6 && !args[6].isEmpty()) { + configBuilder.withModuleInterface(args[6]); + } + var config = configBuilder.build(); + var generator = new Generator(config); + var interpreted = generator.generateResources(); + generator.generateMetaWasm(interpreted); + generator.generateSources(); + if (config.moduleInterface() != null && !config.moduleInterface().isEmpty()) { + generator.generateModuleInterface(config.moduleInterface()); + } + } +} diff --git a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java index ee1c3a82b..d1e8ad898 100644 --- a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java +++ b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java @@ -23,82 +23,55 @@ @Mojo(name = "compile", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true) public class EndiveCompilerGenMojo extends AbstractMojo { - /** - * the wasm module to be used - */ @Parameter(required = true) private File wasmFile; - /** - * the base name to be used for the generated classes - */ @Parameter(required = true) private String name; - /** - * the target folder to generate classes - */ @Parameter( required = true, defaultValue = "${project.build.directory}/generated-resources/endive-compiler") private File targetClassFolder; - /** - * the target source folder to generate the Machine implementation - */ @Parameter( required = true, defaultValue = "${project.build.directory}/generated-sources/endive-compiler") private File targetSourceFolder; - /** - * the target wasm folder to generate the stripped meta wasm module - */ @Parameter( required = true, defaultValue = "${project.build.directory}/generated-resources/endive-compiler") private File targetWasmFolder; - /** - * the action to take if the compiler needs to use the interpreter because a function is too big - */ @Parameter(required = true, defaultValue = "FAIL") InterpreterFallback interpreterFallback; - /** - * The indexes of functions that should be interpreted, separated by commas - */ @Parameter(required = false, defaultValue = "") Set interpretedFunctions; - /** - * Fully qualified name of the user's class that will use the compiled module. - * When set, the plugin generates _ModuleExports and _ModuleImports wrapper classes, - * eliminating the need for @WasmModuleInterface annotation and the annotation processor. - */ @Parameter(required = false) String moduleInterface; /** - * Target triples for Redline native compilation. When set, the plugin - * cross-compiles the Wasm module to native code for each target using - * Cranelift and generates builder()/safeBuilder() methods in the module class. - * Example: x86_64-unknown-linux-gnu, aarch64-apple-darwin + * Enable Redline native compilation (experimental) for all supported + * platforms (x86_64 and aarch64 on Linux, macOS, and Windows). */ - @Parameter(required = false) - List redlineTargets; + @Parameter(required = false, defaultValue = "false") + boolean redlineExperimental; /** - * The target resource folder for native code files (.native). + * Target triples for Redline native compilation. Overrides {@code redlineExperimental} + * for fine-grained control over which platforms to cross-compile for. */ + @Parameter(required = false) + List redlineTargets; + @Parameter( required = true, defaultValue = "${project.build.directory}/generated-resources/endive-compiler") private File targetResourceFolder; - /** - * The current Maven project. - */ @Parameter(property = "project", required = true, readonly = true) private MavenProject project; @@ -119,6 +92,8 @@ public void execute() throws MojoExecutionException { .withTargetResourceFolder(targetResourceFolder.toPath()); if (redlineTargets != null && !redlineTargets.isEmpty()) { configBuilder.withRedlineTargets(redlineTargets); + } else if (redlineExperimental) { + configBuilder.withRedlineTargets(RedlineGenerator.allTargets()); } var config = configBuilder.build(); @@ -130,6 +105,7 @@ public void execute() throws MojoExecutionException { generator.generateSources(); if (config.hasRedlineTargets()) { + getLog().info("Redline native compilation for targets: " + config.redlineTargets()); var redlineGenerator = new RedlineGenerator(config); redlineGenerator.generateNativeCode(); redlineGenerator.extendGeneratedSources(); diff --git a/pom.xml b/pom.xml index a0f75ffff..79a537762 100644 --- a/pom.xml +++ b/pom.xml @@ -1035,13 +1035,6 @@ - - redline - - redline - - - default-all-modules @@ -1067,6 +1060,7 @@ jmh log machine-tests + redline runtime runtime-tests test-gen-lib diff --git a/redline/bridge/pom.xml b/redline/bridge/pom.xml index 784cfdd8a..bbf6c3627 100644 --- a/redline/bridge/pom.xml +++ b/redline/bridge/pom.xml @@ -87,7 +87,7 @@ generate-sources - run.endive.build.time.compiler.Generator + run.endive.build.time.compiler.GeneratorMain ${project.basedir}/../cranelift_bridge.wasm run.endive.redline.experimental.bridge.internal.Cranelift diff --git a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java index f16ed9840..5c5e63f9e 100644 --- a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java +++ b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java @@ -13,6 +13,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import run.endive.build.time.compiler.Config; import run.endive.redline.experimental.api.NativeCodeSerializer; import run.endive.redline.experimental.api.internal.RedlineTarget; @@ -23,6 +26,12 @@ public final class RedlineGenerator { private final Config config; + public static List allTargets() { + return Arrays.stream(RedlineTarget.values()) + .map(RedlineTarget::triple) + .collect(Collectors.toList()); + } + public RedlineGenerator(Config config) { this.config = config; } diff --git a/redline/it/pom.xml b/redline/it/pom.xml new file mode 100644 index 000000000..0a98d80b6 --- /dev/null +++ b/redline/it/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-it-experimental + jar + Endive - Redline IT + Integration tests for the Redline native compiler + + + true + + + + + run.endive + endive-compiler-maven-plugin + + + run.endive + redline-runner-experimental + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + run.endive:endive-compiler-maven-plugin + run.endive:redline-runner-experimental + + + + + org.apache.maven.plugins + maven-invoker-plugin + + ${project.build.directory}/it + true + src/it/settings.xml + verify + true + ${skipTests} + true + invoker.properties + + + + integration-tests + + install + run + + + + + + + diff --git a/redline/it/src/it/redline-e2e/invoker.properties b/redline/it/src/it/redline-e2e/invoker.properties new file mode 100644 index 000000000..84099bc5a --- /dev/null +++ b/redline/it/src/it/redline-e2e/invoker.properties @@ -0,0 +1 @@ +invoker.goals=test diff --git a/redline/it/src/it/redline-e2e/pom.xml b/redline/it/src/it/redline-e2e/pom.xml new file mode 100644 index 000000000..34f210861 --- /dev/null +++ b/redline/it/src/it/redline-e2e/pom.xml @@ -0,0 +1,77 @@ + + + + 4.0.0 + run.endive + redline-e2e-it + 0.0-SNAPSHOT + jar + + + 25 + + + + + run.endive + redline-api-experimental + @project.version@ + + + run.endive + redline-runner-experimental + @project.version@ + + + run.endive + runtime + @project.version@ + + + + org.junit.jupiter + junit-jupiter-api + @junit.version@ + test + + + org.junit.jupiter + junit-jupiter-engine + @junit.version@ + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @maven-compiler-plugin.version@ + + ${maven.compiler.release} + + + + run.endive + endive-compiler-maven-plugin + @project.version@ + + + compile-i32 + + compile + + + endive.test.AddModule + src/test/resources/add.wat.wasm + WARN + true + + + + + + + + diff --git a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java new file mode 100644 index 000000000..d1c06503b --- /dev/null +++ b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java @@ -0,0 +1,53 @@ +package endive.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class RedlineE2eTest { + + @Test + public void nativeBuilderProducesCorrectResults() { + try (var instance = AddModule.builder().build()) { + var add = instance.export("add"); + assertArrayEquals(new long[] {3}, add.apply(1, 2)); + assertArrayEquals(new long[] {0}, add.apply(0, 0)); + assertArrayEquals(new long[] {-1}, add.apply(0, -1)); + } + } + + @Test + public void safeBuilderProducesCorrectResults() { + try (var instance = AddModule.safeBuilder().build()) { + var add = instance.export("add"); + assertArrayEquals(new long[] {3}, add.apply(1, 2)); + assertArrayEquals(new long[] {0}, add.apply(0, 0)); + assertArrayEquals(new long[] {-1}, add.apply(0, -1)); + } + } + + @Test + public void nativeCodeIsAvailable() { + assertNotNull( + AddModule.loadNativeCode(), "Native code should be available on this platform"); + } + + @Test + public void bothBuildersProduceSameResults() { + try (var nativeInstance = AddModule.builder().build(); + var safeInstance = AddModule.safeBuilder().build()) { + var nativeAdd = nativeInstance.export("add"); + var safeAdd = safeInstance.export("add"); + + for (int a = -10; a <= 10; a++) { + for (int b = -10; b <= 10; b++) { + assertArrayEquals( + safeAdd.apply(a, b), + nativeAdd.apply(a, b), + "add(" + a + ", " + b + ") should match"); + } + } + } + } +} diff --git a/redline/it/src/it/redline-e2e/src/test/resources/add.wat.wasm b/redline/it/src/it/redline-e2e/src/test/resources/add.wat.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ad1f2f7a3e5ef7d1729d54d08e504be1ef891ae5 GIT binary patch literal 67 zcmWm3u?>JQ3`N2J9E25EfHI?Dlr%`8VF0g=xb_t9L=w(vnPf5KPKn;7t>S8G+`EqD Oi8)x&e^!x+fa?Q1WC;TR literal 0 HcmV?d00001 diff --git a/redline/it/src/it/settings.xml b/redline/it/src/it/settings.xml new file mode 100644 index 000000000..2d90068bb --- /dev/null +++ b/redline/it/src/it/settings.xml @@ -0,0 +1,35 @@ + + + + + it-repo + + true + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + From d76f175413a028fcc3c6dbd55360a8938a188c23 Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 10 Aug 2026 11:51:05 +0200 Subject: [PATCH 03/20] Remove -Predline profile, redline always builds - Add Rust toolchain + cranelift_bridge.wasm build to ci.yaml - Remove -Predline from release.yaml and redline.yaml - Plugin depends directly on redline-build-time-compiler (no reflection) - Bridge uses exec:java with GeneratorMain to break the cycle - Generated code uses NativeMachineFactoryProvider SPI (no Panama import) - Remove redundant targetResourceFolder from Config - Validate target triples before compilation in RedlineGenerator - E2E tests: JFFI (all JDKs) + Panama (JDK 25+ only) - Restore javadoc comments on Mojo fields --- .github/workflows/ci.yaml | 7 ++ .github/workflows/redline.yaml | 2 +- .github/workflows/release.yaml | 12 +-- .../endive/build/time/compiler/Config.java | 22 +----- .../time/maven/EndiveCompilerGenMojo.java | 37 +++++++-- redline/bridge/pom.xml | 7 +- .../experimental/build/RedlineGenerator.java | 16 ++-- redline/it/pom.xml | 5 ++ .../it/redline-e2e-panama/invoker.properties | 2 + redline/it/src/it/redline-e2e-panama/pom.xml | 73 ++++++++++++++++++ .../endive/test/RedlinePanamaE2eTest.java | 56 ++++++++++++++ .../src/test/resources/add.wat.wasm | Bin 0 -> 67 bytes redline/it/src/it/redline-e2e/pom.xml | 8 +- .../test/java/endive/test/RedlineE2eTest.java | 26 +++++-- redline/pom.xml | 2 +- 15 files changed, 215 insertions(+), 60 deletions(-) create mode 100644 redline/it/src/it/redline-e2e-panama/invoker.properties create mode 100644 redline/it/src/it/redline-e2e-panama/pom.xml create mode 100644 redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java create mode 100644 redline/it/src/it/redline-e2e-panama/src/test/resources/add.wat.wasm diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ce1816efa..88e43254c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,6 +47,13 @@ jobs: distribution: 'temurin' java-version: '${{ matrix.version }}' cache: maven + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + - name: Build cranelift_bridge.wasm + working-directory: redline/wasm-build + run: make all - name: Test Java run: ./mvnw -B clean install env: diff --git a/.github/workflows/redline.yaml b/.github/workflows/redline.yaml index fda288f84..088b1f864 100644 --- a/.github/workflows/redline.yaml +++ b/.github/workflows/redline.yaml @@ -36,6 +36,6 @@ jobs: working-directory: redline/wasm-build run: make all - name: Build and test redline - run: ./mvnw -B clean install -Predline + run: ./mvnw -B clean install env: MAVEN_OPTS: "-ea" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 84c647e4f..1dcdfd2b6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -53,8 +53,7 @@ jobs: run: make all - name: Compile - run: ./mvnw --batch-mode -Dquickly -Predline - + run: ./mvnw --batch-mode -Dquickly - name: Setup Git run: | git config user.name "Endive BOT" @@ -62,8 +61,7 @@ jobs: - name: Set the version run: | - ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} -Predline - git add . + ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} git add . git commit -m "Release version update ${{ github.event.inputs.release-version }}" git push git tag ${{ github.event.inputs.release-version }} @@ -74,8 +72,7 @@ jobs: - name: Release to Maven Central run: | # -Dquickly is needed to locally publish wasm-corpus - ./mvnw --batch-mode -Dquickly -Predline - ./mvnw --batch-mode clean deploy -Drelease -Predline -DskipTests=true -X + ./mvnw --batch-mode -Dquickly ./mvnw --batch-mode clean deploy -Drelease -Predline -DskipTests=true -X env: MAVEN_USERNAME: ${{ secrets.SONATYPE_USERNAME }} MAVEN_CENTRAL_TOKEN: ${{ secrets.SONATYPE_PASSWORD }} @@ -83,8 +80,7 @@ jobs: - name: Back to Snapshot run: | - ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT -Predline - git add . + ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT git add . git commit -m "Snapshot version update" git push env: diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java index 22a870d6e..5d6a7c484 100644 --- a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java @@ -52,11 +52,6 @@ public final class Config { */ private final List redlineTargets; - /** - * the target resource folder for native code files - */ - private final Path targetResourceFolder; - private Config( Path wasmFile, String name, @@ -66,8 +61,7 @@ private Config( InterpreterFallback interpreterFallback, Set interpretedFunctions, String moduleInterface, - List redlineTargets, - Path targetResourceFolder) { + List redlineTargets) { this.wasmFile = wasmFile; this.name = name; this.targetClassFolder = targetClassFolder; @@ -77,7 +71,6 @@ private Config( this.interpretedFunctions = interpretedFunctions; this.moduleInterface = moduleInterface; this.redlineTargets = redlineTargets; - this.targetResourceFolder = targetResourceFolder; } public Path wasmFile() { @@ -116,10 +109,6 @@ public List redlineTargets() { return redlineTargets; } - public Path targetResourceFolder() { - return targetResourceFolder; - } - public boolean hasRedlineTargets() { return redlineTargets != null && !redlineTargets.isEmpty(); } @@ -154,7 +143,6 @@ public static final class Builder { private Set interpretedFunctions; private String moduleInterface; private List redlineTargets = List.of(); - private Path targetResourceFolder; private Builder() {} @@ -203,11 +191,6 @@ public Builder withRedlineTargets(List redlineTargets) { return this; } - public Builder withTargetResourceFolder(Path targetResourceFolder) { - this.targetResourceFolder = targetResourceFolder; - return this; - } - public Config build() { return new Config( wasmFile, @@ -218,8 +201,7 @@ public Config build() { interpreterFallback, interpretedFunctions, moduleInterface, - redlineTargets, - targetResourceFolder); + redlineTargets); } } } diff --git a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java index d1e8ad898..371c37367 100644 --- a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java +++ b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java @@ -23,33 +23,59 @@ @Mojo(name = "compile", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true) public class EndiveCompilerGenMojo extends AbstractMojo { + /** + * the wasm module to be used + */ @Parameter(required = true) private File wasmFile; + /** + * the base name to be used for the generated classes + */ @Parameter(required = true) private String name; + /** + * the target folder to generate classes + */ @Parameter( required = true, defaultValue = "${project.build.directory}/generated-resources/endive-compiler") private File targetClassFolder; + /** + * the target source folder to generate the Machine implementation + */ @Parameter( required = true, defaultValue = "${project.build.directory}/generated-sources/endive-compiler") private File targetSourceFolder; + /** + * the target wasm folder to generate the stripped meta wasm module + */ @Parameter( required = true, defaultValue = "${project.build.directory}/generated-resources/endive-compiler") private File targetWasmFolder; + /** + * the action to take if the compiler needs to use the interpreter because a function is too big + */ @Parameter(required = true, defaultValue = "FAIL") InterpreterFallback interpreterFallback; + /** + * The indexes of functions that should be interpreted, separated by commas + */ @Parameter(required = false, defaultValue = "") Set interpretedFunctions; + /** + * Fully qualified name of the user's class that will use the compiled module. + * When set, the plugin generates _ModuleExports and _ModuleImports wrapper classes, + * eliminating the need for @WasmModuleInterface annotation and the annotation processor. + */ @Parameter(required = false) String moduleInterface; @@ -67,11 +93,9 @@ public class EndiveCompilerGenMojo extends AbstractMojo { @Parameter(required = false) List redlineTargets; - @Parameter( - required = true, - defaultValue = "${project.build.directory}/generated-resources/endive-compiler") - private File targetResourceFolder; - + /** + * The current Maven project. + */ @Parameter(property = "project", required = true, readonly = true) private MavenProject project; @@ -88,8 +112,7 @@ public void execute() throws MojoExecutionException { .withTargetWasmFolder(targetWasmFolder.toPath()) .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) - .withModuleInterface(moduleInterface) - .withTargetResourceFolder(targetResourceFolder.toPath()); + .withModuleInterface(moduleInterface); if (redlineTargets != null && !redlineTargets.isEmpty()) { configBuilder.withRedlineTargets(redlineTargets); } else if (redlineExperimental) { diff --git a/redline/bridge/pom.xml b/redline/bridge/pom.xml index bbf6c3627..dd1487f37 100644 --- a/redline/bridge/pom.xml +++ b/redline/bridge/pom.xml @@ -30,10 +30,9 @@ diff --git a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java index 5c5e63f9e..6126293a8 100644 --- a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java +++ b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java @@ -43,18 +43,17 @@ public void generateNativeCode() throws IOException { var module = Parser.parse(config.wasmFile()); var packagePath = config.getPackageName().replace('.', '/'); var baseName = config.getBaseName(); - var resourceDir = config.targetResourceFolder().resolve(packagePath); + var resourceDir = config.targetClassFolder().resolve(packagePath); Files.createDirectories(resourceDir); for (String triple : config.redlineTargets()) { - byte[][] compiledCode = NativeCompiler.compileAll(triple, module); - var target = RedlineTarget.fromTriple(triple) .orElseThrow( () -> new IllegalArgumentException( "Unknown target triple: " + triple)); + byte[][] compiledCode = NativeCompiler.compileAll(triple, module); var nativeFile = resourceDir.resolve(baseName + "." + target.resourceSuffix() + ".native"); @@ -77,8 +76,8 @@ public void extendGeneratedSources() throws IOException { var type = cu.getClassByName(baseName).orElseThrow(); cu.addImport("run.endive.redline.experimental.api.NativeCodeSerializer"); + cu.addImport("run.endive.redline.experimental.api.NativeMachineFactoryProvider"); cu.addImport("run.endive.redline.experimental.api.internal.RedlineTarget"); - cu.addImport("run.endive.redline.experimental.runner.NativeMachineFactory"); cu.addImport("java.io.InputStream"); cu.addImport("java.io.IOException"); cu.addImport("java.io.UncheckedIOException"); @@ -114,7 +113,7 @@ private static void generateNativeCodeHolderInnerClass( initBody.addStatement( StaticJavaParser.parseStatement( - "if (host == null || Runtime.version().feature() < 25) {\n" + "if (host == null) {\n" + " CODE = null;\n" + "} else {\n" + " String resource = \"" @@ -154,9 +153,10 @@ private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, Stri body.addStatement( StaticJavaParser.parseStatement( "if (nativeCode != null) {\n" - + " return NativeMachineFactory.builder(module)" - + ".withPrecompiledCode(nativeCode)" - + ".toInstanceBuilder();\n" + + " var provider = NativeMachineFactoryProvider.discover();\n" + + " if (provider.isPresent()) {\n" + + " return provider.get().builder(module, nativeCode);\n" + + " }\n" + "}")); body.addStatement( StaticJavaParser.parseStatement( diff --git a/redline/it/pom.xml b/redline/it/pom.xml index 0a98d80b6..56b4a20da 100644 --- a/redline/it/pom.xml +++ b/redline/it/pom.xml @@ -26,6 +26,10 @@ run.endive redline-runner-experimental + + run.endive + redline-runner-jffi-experimental + @@ -36,6 +40,7 @@ run.endive:endive-compiler-maven-plugin + run.endive:redline-runner-jffi-experimental run.endive:redline-runner-experimental diff --git a/redline/it/src/it/redline-e2e-panama/invoker.properties b/redline/it/src/it/redline-e2e-panama/invoker.properties new file mode 100644 index 000000000..d89167bdd --- /dev/null +++ b/redline/it/src/it/redline-e2e-panama/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals=test +invoker.java.version=25+ diff --git a/redline/it/src/it/redline-e2e-panama/pom.xml b/redline/it/src/it/redline-e2e-panama/pom.xml new file mode 100644 index 000000000..cab4de5c8 --- /dev/null +++ b/redline/it/src/it/redline-e2e-panama/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + run.endive + redline-e2e-panama-it + 0.0-SNAPSHOT + jar + + + + run.endive + redline-api-experimental + @project.version@ + + + run.endive + redline-runner-experimental + @project.version@ + + + run.endive + runtime + @project.version@ + + + + org.junit.jupiter + junit-jupiter-api + @junit.version@ + test + + + org.junit.jupiter + junit-jupiter-engine + @junit.version@ + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @maven-compiler-plugin.version@ + + 25 + + + + run.endive + endive-compiler-maven-plugin + @project.version@ + + + compile-i32 + + compile + + + endive.test.AddModule + src/test/resources/add.wat.wasm + WARN + true + + + + + + + + diff --git a/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java new file mode 100644 index 000000000..7f83f2df7 --- /dev/null +++ b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java @@ -0,0 +1,56 @@ +package endive.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import run.endive.redline.experimental.api.NativeMachineFactoryProvider; + +class RedlinePanamaE2eTest { + + @Test + public void panamaProviderIsSelected() { + var provider = NativeMachineFactoryProvider.discover(); + assertTrue(provider.isPresent(), "Should discover a native provider"); + assertEquals(100, provider.get().priority(), "Panama should win with priority 100"); + } + + @Test + public void nativeBuilderProducesCorrectResults() { + try (var instance = AddModule.builder().build()) { + var add = instance.export("add"); + assertArrayEquals(new long[] {3}, add.apply(1, 2)); + assertArrayEquals(new long[] {0}, add.apply(0, 0)); + assertEquals( + (int) add.apply(0, -1)[0], + -1, + "i32 add(0, -1) should be -1 when narrowed to int"); + } + } + + @Test + public void nativeCodeIsAvailable() { + assertNotNull( + AddModule.loadNativeCode(), "Native code should be available on this platform"); + } + + @Test + public void bothBuildersProduceSameResults() { + try (var nativeInstance = AddModule.builder().build(); + var safeInstance = AddModule.safeBuilder().build()) { + var nativeAdd = nativeInstance.export("add"); + var safeAdd = safeInstance.export("add"); + + for (int a = -10; a <= 10; a++) { + for (int b = -10; b <= 10; b++) { + assertEquals( + (int) safeAdd.apply(a, b)[0], + (int) nativeAdd.apply(a, b)[0], + "add(" + a + ", " + b + ") should match"); + } + } + } + } +} diff --git a/redline/it/src/it/redline-e2e-panama/src/test/resources/add.wat.wasm b/redline/it/src/it/redline-e2e-panama/src/test/resources/add.wat.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ad1f2f7a3e5ef7d1729d54d08e504be1ef891ae5 GIT binary patch literal 67 zcmWm3u?>JQ3`N2J9E25EfHI?Dlr%`8VF0g=xb_t9L=w(vnPf5KPKn;7t>S8G+`EqD Oi8)x&e^!x+fa?Q1WC;TR literal 0 HcmV?d00001 diff --git a/redline/it/src/it/redline-e2e/pom.xml b/redline/it/src/it/redline-e2e/pom.xml index 34f210861..09c52c649 100644 --- a/redline/it/src/it/redline-e2e/pom.xml +++ b/redline/it/src/it/redline-e2e/pom.xml @@ -7,10 +7,6 @@ 0.0-SNAPSHOT jar - - 25 - - run.endive @@ -19,7 +15,7 @@ run.endive - redline-runner-experimental + redline-runner-jffi-experimental @project.version@ @@ -49,7 +45,7 @@ maven-compiler-plugin @maven-compiler-plugin.version@ - ${maven.compiler.release} + 11 diff --git a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java index d1c06503b..baf2dd4f9 100644 --- a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java +++ b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java @@ -1,19 +1,32 @@ package endive.test; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import run.endive.redline.experimental.api.NativeMachineFactoryProvider; class RedlineE2eTest { + @Test + public void jffiProviderIsSelected() { + var provider = NativeMachineFactoryProvider.discover(); + assertTrue(provider.isPresent(), "Should discover a native provider"); + assertEquals(50, provider.get().priority(), "JFFI should be selected with priority 50"); + } + @Test public void nativeBuilderProducesCorrectResults() { try (var instance = AddModule.builder().build()) { var add = instance.export("add"); assertArrayEquals(new long[] {3}, add.apply(1, 2)); assertArrayEquals(new long[] {0}, add.apply(0, 0)); - assertArrayEquals(new long[] {-1}, add.apply(0, -1)); + assertEquals( + (int) add.apply(0, -1)[0], + -1, + "i32 add(0, -1) should be -1 when narrowed to int"); } } @@ -23,7 +36,10 @@ public void safeBuilderProducesCorrectResults() { var add = instance.export("add"); assertArrayEquals(new long[] {3}, add.apply(1, 2)); assertArrayEquals(new long[] {0}, add.apply(0, 0)); - assertArrayEquals(new long[] {-1}, add.apply(0, -1)); + assertEquals( + (int) add.apply(0, -1)[0], + -1, + "i32 add(0, -1) should be -1 when narrowed to int"); } } @@ -42,9 +58,9 @@ public void bothBuildersProduceSameResults() { for (int a = -10; a <= 10; a++) { for (int b = -10; b <= 10; b++) { - assertArrayEquals( - safeAdd.apply(a, b), - nativeAdd.apply(a, b), + assertEquals( + (int) safeAdd.apply(a, b)[0], + (int) nativeAdd.apply(a, b)[0], "add(" + a + ", " + b + ") should match"); } } diff --git a/redline/pom.xml b/redline/pom.xml index e8c0f3c7c..2c617995f 100644 --- a/redline/pom.xml +++ b/redline/pom.xml @@ -19,6 +19,7 @@ bridge build-time-compiler compiler + it runner-jffi runner-jffi-tests @@ -30,7 +31,6 @@ [25,) - it runner runner-tests From 5e73e8e550aac373ad5e3e3425e2f23c6a7e6f0a Mon Sep 17 00:00:00 2001 From: andreatp Date: Wed, 12 Aug 2026 15:57:06 +0200 Subject: [PATCH 04/20] Use inlay to fetch cranelift_bridge.wasm from GHCR Replace Rust toolchain + make all in CI with inlay-maven-plugin fetching the pre-built wasm from ghcr.io/bytecodealliance/endive-cranelift-bridge. No Rust needed in any CI workflow except the new wasm-publish.yaml. - Bridge POM: inlay:fetch before exec:java GeneratorMain - Root POM: add inlay-maven-plugin to pluginManagement - CI/redline/release workflows: remove Rust toolchain steps - New wasm-publish.yaml: build + publish wasm to GHCR on wasm-build changes - wkg.lock: pins wasm digest for reproducible builds --- .github/workflows/ci.yaml | 7 ------ .github/workflows/redline.yaml | 7 ------ .github/workflows/release.yaml | 9 ------- .github/workflows/wasm-publish.yaml | 38 +++++++++++++++++++++++++++++ pom.xml | 6 +++++ redline/bridge/pom.xml | 26 +++++++++++++++++--- redline/wkg.lock | 12 +++++++++ 7 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/wasm-publish.yaml create mode 100644 redline/wkg.lock diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 88e43254c..ce1816efa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,13 +47,6 @@ jobs: distribution: 'temurin' java-version: '${{ matrix.version }}' cache: maven - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - name: Build cranelift_bridge.wasm - working-directory: redline/wasm-build - run: make all - name: Test Java run: ./mvnw -B clean install env: diff --git a/.github/workflows/redline.yaml b/.github/workflows/redline.yaml index 088b1f864..961a7861b 100644 --- a/.github/workflows/redline.yaml +++ b/.github/workflows/redline.yaml @@ -28,13 +28,6 @@ jobs: distribution: 'temurin' java-version: '25' cache: maven - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - name: Build cranelift_bridge.wasm - working-directory: redline/wasm-build - run: make all - name: Build and test redline run: ./mvnw -B clean install env: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1dcdfd2b6..dccc0f033 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -37,21 +37,12 @@ jobs: gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - id: install-secret-key name: Install gpg secret key run: | cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import gpg --list-secret-keys --keyid-format LONG - - name: Build cranelift_bridge.wasm - working-directory: redline/wasm-build - run: make all - - name: Compile run: ./mvnw --batch-mode -Dquickly - name: Setup Git diff --git a/.github/workflows/wasm-publish.yaml b/.github/workflows/wasm-publish.yaml new file mode 100644 index 000000000..b95b8e27b --- /dev/null +++ b/.github/workflows/wasm-publish.yaml @@ -0,0 +1,38 @@ +name: Publish cranelift_bridge.wasm + +on: + push: + branches: [main] + paths: ['redline/wasm-build/**'] + workflow_dispatch: + +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + steps: + - name: Checkout sources + uses: actions/checkout@v7 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + + - name: Build cranelift_bridge.wasm + working-directory: redline/wasm-build + run: make all + + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + - name: Login to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push to GHCR + run: | + oras push ghcr.io/bytecodealliance/endive-cranelift-bridge:latest \ + redline/cranelift_bridge.wasm:application/wasm diff --git a/pom.xml b/pom.xml index 79a537762..d125ddf78 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ 3.10.1 0.11.0 3.6.3 + 0.0.2 3.9.0 3.1.0 3.2.0 @@ -414,6 +415,11 @@ spotless-maven-plugin ${spotless-maven-plugin.version} + + io.roastedroot + inlay-maven-plugin + ${inlay-maven-plugin.version} + org.apache.maven.plugins maven-antrun-plugin diff --git a/redline/bridge/pom.xml b/redline/bridge/pom.xml index dd1487f37..eb32af6c7 100644 --- a/redline/bridge/pom.xml +++ b/redline/bridge/pom.xml @@ -30,11 +30,31 @@ + + io.roastedroot + inlay-maven-plugin + + + fetch-cranelift-bridge + + fetch + + + + + ghcr.io/bytecodealliance/endive-cranelift-bridge:999.0.0-SNAPSHOT + ${project.basedir}/../cranelift_bridge.wasm + + + ${project.basedir}/../wkg.lock + + + + org.codehaus.mojo build-helper-maven-plugin diff --git a/redline/wkg.lock b/redline/wkg.lock new file mode 100644 index 000000000..bd266a4ab --- /dev/null +++ b/redline/wkg.lock @@ -0,0 +1,12 @@ +# This file is automatically generated. +# It is not intended for manual editing. +version = 1 + +[[packages]] +name = "bytecodealliance:endive-cranelift-bridge" +registry = "ghcr.io" + +[[packages.versions]] +requirement = "=999.0.0-SNAPSHOT" +version = "999.0.0-SNAPSHOT" +digest = "sha256:2436e889dae98422313f495632557068dda466e8170aa718a3e70d7fee1d652f" From f9e1b7dd22b2a77e3f526be0f2a4f60f3fb7cf03 Mon Sep 17 00:00:00 2001 From: andreatp Date: Fri, 14 Aug 2026 11:22:56 +0200 Subject: [PATCH 05/20] Remove Panama runner dep from it module The it module is unconditional (builds on all JDKs) but redline-runner-experimental requires JDK 25+. The Panama E2E test project declares its own dependency via @project.version@. --- redline/it/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/redline/it/pom.xml b/redline/it/pom.xml index 56b4a20da..91c36dc9d 100644 --- a/redline/it/pom.xml +++ b/redline/it/pom.xml @@ -22,10 +22,6 @@ run.endive endive-compiler-maven-plugin - - run.endive - redline-runner-experimental - run.endive redline-runner-jffi-experimental @@ -41,7 +37,6 @@ run.endive:endive-compiler-maven-plugin run.endive:redline-runner-jffi-experimental - run.endive:redline-runner-experimental From 4fdfa28c62288bc098004b52da8e5d030474500e Mon Sep 17 00:00:00 2001 From: andreatp Date: Fri, 14 Aug 2026 18:47:40 +0200 Subject: [PATCH 06/20] Gate Panama runner dep in it module behind java25 profile The it module is unconditional but redline-runner-experimental requires JDK 25+. Move it to a java25 profile so it only resolves on JDK 25+ where the Panama E2E test actually runs. --- redline/it/pom.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/redline/it/pom.xml b/redline/it/pom.xml index 91c36dc9d..42e7fb2ee 100644 --- a/redline/it/pom.xml +++ b/redline/it/pom.xml @@ -37,6 +37,7 @@ run.endive:endive-compiler-maven-plugin run.endive:redline-runner-jffi-experimental + run.endive:redline-runner-experimental @@ -65,4 +66,19 @@ + + + + java25 + + [25,) + + + + run.endive + redline-runner-experimental + + + + From b7908cf03545ae8860feb5200b79e7ab5d2836c2 Mon Sep 17 00:00:00 2001 From: andreatp Date: Fri, 21 Aug 2026 17:18:03 +0100 Subject: [PATCH 07/20] bump inlay --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d125ddf78..496719378 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ 3.10.1 0.11.0 3.6.3 - 0.0.2 + 0.0.3 3.9.0 3.1.0 3.2.0 From e3c0652c9e11d34c221cacf5dd8a8a6a749cbf8b Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 24 Aug 2026 10:32:37 +0100 Subject: [PATCH 08/20] Rename redlineTargets to redlineTargetsExperimental MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the redlineExperimental flag naming — both are user-facing plugin parameters for the experimental redline feature. --- .../run/endive/build/time/maven/EndiveCompilerGenMojo.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java index 371c37367..cf332d091 100644 --- a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java +++ b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java @@ -91,7 +91,7 @@ public class EndiveCompilerGenMojo extends AbstractMojo { * for fine-grained control over which platforms to cross-compile for. */ @Parameter(required = false) - List redlineTargets; + List redlineTargetsExperimental; /** * The current Maven project. @@ -113,8 +113,8 @@ public void execute() throws MojoExecutionException { .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) .withModuleInterface(moduleInterface); - if (redlineTargets != null && !redlineTargets.isEmpty()) { - configBuilder.withRedlineTargets(redlineTargets); + if (redlineTargetsExperimental != null && !redlineTargetsExperimental.isEmpty()) { + configBuilder.withRedlineTargets(redlineTargetsExperimental); } else if (redlineExperimental) { configBuilder.withRedlineTargets(RedlineGenerator.allTargets()); } From c52f0667453450870adec1f99eba76d0757118d4 Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 24 Aug 2026 11:21:06 +0100 Subject: [PATCH 09/20] Build generated code with JavaParser AST nodes, not parseStatement Replace parseStatement + string concatenation with direct AST node construction, matching the idiom already used by build-time-compiler's Generator (which has zero parse calls in its codegen). Each generator method now carries a comment showing the code it emits. Generated output is unchanged. --- .../experimental/build/RedlineGenerator.java | 267 +++++++++++++++--- 1 file changed, 224 insertions(+), 43 deletions(-) diff --git a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java index 6126293a8..6e680b4c3 100644 --- a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java +++ b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java @@ -7,9 +7,28 @@ import com.github.javaparser.ast.Modifier; import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.BinaryExpr; +import com.github.javaparser.ast.expr.ClassExpr; +import com.github.javaparser.ast.expr.ConditionalExpr; import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.NullLiteralExpr; +import com.github.javaparser.ast.expr.ObjectCreationExpr; +import com.github.javaparser.ast.expr.StringLiteralExpr; +import com.github.javaparser.ast.expr.VariableDeclarationExpr; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.CatchClause; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.IfStmt; import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.ThrowStmt; +import com.github.javaparser.ast.stmt.TryStmt; +import com.github.javaparser.ast.type.VarType; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -93,6 +112,26 @@ public void extendGeneratedSources() throws IOException { private static void generateNativeCodeHolderInnerClass( ClassOrInterfaceDeclaration type, String moduleName) { + // Generates: + // + // private static class NativeCodeHolder { + // static final byte[][] CODE; + // static { + // var host = RedlineTarget.detectHost().orElse(null); + // if (host == null) { + // CODE = null; + // } else { + // String resource = "." + host.resourceSuffix() + ".native"; + // try (InputStream in = + // .class.getResourceAsStream(resource)) { + // CODE = (in == null) ? null : NativeCodeSerializer.deserialize(in); + // } catch (IOException e) { + // throw new UncheckedIOException("Failed to load native code", e); + // } + // } + // } + // } + // var holderClass = new ClassOrInterfaceDeclaration( NodeList.nodeList( @@ -105,33 +144,108 @@ private static void generateNativeCodeHolderInnerClass( holderClass.addField( parseType("byte[][]"), "CODE", Modifier.Keyword.STATIC, Modifier.Keyword.FINAL); - var initBody = holderClass.addStaticInitializer(); + // var host = RedlineTarget.detectHost().orElse(null); + var detectHost = + new MethodCallExpr( + new MethodCallExpr(new NameExpr("RedlineTarget"), "detectHost"), + "orElse", + new NodeList<>(new NullLiteralExpr())); + var hostVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator(new VarType(), "host", detectHost))); + + // CODE = null; + var assignNull = + new ExpressionStmt( + new AssignExpr( + new NameExpr("CODE"), + new NullLiteralExpr(), + AssignExpr.Operator.ASSIGN)); + + // String resource = "." + host.resourceSuffix() + ".native"; + var resourceName = + new BinaryExpr( + new BinaryExpr( + new StringLiteralExpr(moduleName + "."), + new MethodCallExpr(new NameExpr("host"), "resourceSuffix"), + BinaryExpr.Operator.PLUS), + new StringLiteralExpr(".native"), + BinaryExpr.Operator.PLUS); + var resourceVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + parseClassOrInterfaceType("String"), + "resource", + resourceName))); + + // try (InputStream in = .class.getResourceAsStream(resource)) + var getResource = + new MethodCallExpr( + new ClassExpr(parseType(moduleName)), + "getResourceAsStream", + new NodeList<>(new NameExpr("resource"))); + var streamResource = + new VariableDeclarationExpr( + new VariableDeclarator(parseType("InputStream"), "in", getResource)); + + // CODE = (in == null) ? null : NativeCodeSerializer.deserialize(in); + var deserialize = + new ConditionalExpr( + new BinaryExpr( + new NameExpr("in"), + new NullLiteralExpr(), + BinaryExpr.Operator.EQUALS), + new NullLiteralExpr(), + new MethodCallExpr( + new NameExpr("NativeCodeSerializer"), + "deserialize", + new NodeList<>(new NameExpr("in")))); + var assignCode = + new ExpressionStmt( + new AssignExpr( + new NameExpr("CODE"), deserialize, AssignExpr.Operator.ASSIGN)); - initBody.addStatement( - StaticJavaParser.parseStatement( - "var host = RedlineTarget.detectHost().orElse(null);")); - - initBody.addStatement( - StaticJavaParser.parseStatement( - "if (host == null) {\n" - + " CODE = null;\n" - + "} else {\n" - + " String resource = \"" - + moduleName - + ".\" + host.resourceSuffix() + \".native\";\n" - + " try (InputStream in = " - + moduleName - + ".class.getResourceAsStream(resource)) {\n" - + " CODE = (in == null) ? null" - + " : NativeCodeSerializer.deserialize(in);\n" - + " } catch (IOException e) {\n" - + " throw new UncheckedIOException(" - + "\"Failed to load native code\", e);\n" - + " }\n" - + "}")); + // catch (IOException e) { throw new UncheckedIOException("...", e); } + var newException = + new ObjectCreationExpr() + .setType(parseClassOrInterfaceType("UncheckedIOException")) + .addArgument(new StringLiteralExpr("Failed to load native code")) + .addArgument(new NameExpr("e")); + var catchIoException = + new CatchClause() + .setParameter(new Parameter(parseClassOrInterfaceType("IOException"), "e")) + .setBody(new BlockStmt(new NodeList<>(new ThrowStmt(newException)))); + + var tryLoad = + new TryStmt() + .setResources(new NodeList<>(streamResource)) + .setTryBlock(new BlockStmt(new NodeList<>(assignCode))) + .setCatchClauses(new NodeList<>(catchIoException)); + + var loadFromResource = + new IfStmt() + .setCondition( + new BinaryExpr( + new NameExpr("host"), + new NullLiteralExpr(), + BinaryExpr.Operator.EQUALS)) + .setThenStmt(new BlockStmt(new NodeList<>(assignNull))) + .setElseStmt(new BlockStmt(new NodeList<>(resourceVar, tryLoad))); + + var initBody = holderClass.addStaticInitializer(); + initBody.addStatement(hostVar); + initBody.addStatement(loadFromResource); } private static void generateLoadNativeCodeMethod(ClassOrInterfaceDeclaration type) { + // Generates: + // + // public static byte[][] loadNativeCode() { + // return NativeCodeHolder.CODE; + // } + // var method = type.addMethod("loadNativeCode", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) .setType(parseType("byte[][]")); @@ -142,40 +256,107 @@ private static void generateLoadNativeCodeMethod(ClassOrInterfaceDeclaration typ } private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, String moduleName) { + // Generates: + // + // public static Instance.Builder builder() { + // var module = load(); + // byte[][] nativeCode = loadNativeCode(); + // if (nativeCode != null) { + // var provider = NativeMachineFactoryProvider.discover(); + // if (provider.isPresent()) { + // return provider.get().builder(module, nativeCode); + // } + // } + // return Instance.builder(module).withMachineFactory(::create); + // } + // var method = type.addMethod("builder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) .setType(parseClassOrInterfaceType("Instance.Builder")); + // var module = load(); + var moduleVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + new VarType(), "module", new MethodCallExpr("load")))); + + // byte[][] nativeCode = loadNativeCode(); + var nativeCodeVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + parseType("byte[][]"), + "nativeCode", + new MethodCallExpr("loadNativeCode")))); + + // var provider = NativeMachineFactoryProvider.discover(); + var providerVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + new VarType(), + "provider", + new MethodCallExpr( + new NameExpr("NativeMachineFactoryProvider"), + "discover")))); + + // return provider.get().builder(module, nativeCode); + var returnNative = + new ReturnStmt( + new MethodCallExpr( + new MethodCallExpr(new NameExpr("provider"), "get"), + "builder", + new NodeList<>( + new NameExpr("module"), new NameExpr("nativeCode")))); + + var ifProviderPresent = + new IfStmt() + .setCondition(new MethodCallExpr(new NameExpr("provider"), "isPresent")) + .setThenStmt(new BlockStmt(new NodeList<>(returnNative))); + + var ifNativeCode = + new IfStmt() + .setCondition( + new BinaryExpr( + new NameExpr("nativeCode"), + new NullLiteralExpr(), + BinaryExpr.Operator.NOT_EQUALS)) + .setThenStmt(new BlockStmt(new NodeList<>(providerVar, ifProviderPresent))); + var body = method.createBody(); - body.addStatement(StaticJavaParser.parseStatement("var module = load();")); - body.addStatement( - StaticJavaParser.parseStatement("byte[][] nativeCode = loadNativeCode();")); - body.addStatement( - StaticJavaParser.parseStatement( - "if (nativeCode != null) {\n" - + " var provider = NativeMachineFactoryProvider.discover();\n" - + " if (provider.isPresent()) {\n" - + " return provider.get().builder(module, nativeCode);\n" - + " }\n" - + "}")); - body.addStatement( - StaticJavaParser.parseStatement( - "return Instance.builder(module).withMachineFactory(" - + moduleName - + "::create);")); + body.addStatement(moduleVar); + body.addStatement(nativeCodeVar); + body.addStatement(ifNativeCode); + body.addStatement(new ReturnStmt(interpreterBuilder(new NameExpr("module"), moduleName))); } private static void generateSafeBuilderMethod( ClassOrInterfaceDeclaration type, String moduleName) { + // Generates: + // + // public static Instance.Builder safeBuilder() { + // return Instance.builder(load()).withMachineFactory(::create); + // } + // var method = type.addMethod("safeBuilder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) .setType(parseClassOrInterfaceType("Instance.Builder")); method.createBody() .addStatement( - StaticJavaParser.parseStatement( - "return Instance.builder(load()).withMachineFactory(" - + moduleName - + "::create);")); + new ReturnStmt(interpreterBuilder(new MethodCallExpr("load"), moduleName))); + } + + /** {@code Instance.builder().withMachineFactory(::create)} */ + private static MethodCallExpr interpreterBuilder( + com.github.javaparser.ast.expr.Expression module, String moduleName) { + return new MethodCallExpr( + new MethodCallExpr(new NameExpr("Instance"), "builder", new NodeList<>(module)), + "withMachineFactory", + new NodeList<>( + new MethodReferenceExpr() + .setScope(new NameExpr(moduleName)) + .setIdentifier("create"))); } } From d842559a00a3be8228538269e80e7c225b12a408 Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 24 Aug 2026 11:28:57 +0100 Subject: [PATCH 10/20] Address PR review: minimal E2E POMs, platform-independent assertion - Drop redline-api and runtime from the E2E POMs; both come transitively from the runner, leaving a single consumer dependency - Drop interpreterFallback=WARN; the trivial add module never needs it and FAIL (the default) is the stricter check - Guard nativeCodeIsAvailable with an assumption on detectHost(), so it skips rather than fails on platforms Redline does not target --- redline/it/src/it/redline-e2e-panama/pom.xml | 14 +++----------- .../java/endive/test/RedlinePanamaE2eTest.java | 6 ++++++ redline/it/src/it/redline-e2e/pom.xml | 14 +++----------- .../src/test/java/endive/test/RedlineE2eTest.java | 6 ++++++ 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/redline/it/src/it/redline-e2e-panama/pom.xml b/redline/it/src/it/redline-e2e-panama/pom.xml index cab4de5c8..65632de54 100644 --- a/redline/it/src/it/redline-e2e-panama/pom.xml +++ b/redline/it/src/it/redline-e2e-panama/pom.xml @@ -8,21 +8,13 @@ jar - - run.endive - redline-api-experimental - @project.version@ - + run.endive redline-runner-experimental @project.version@ - - run.endive - runtime - @project.version@ - org.junit.jupiter @@ -36,6 +28,7 @@ @junit.version@ test + @@ -61,7 +54,6 @@ endive.test.AddModule src/test/resources/add.wat.wasm - WARN true diff --git a/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java index 7f83f2df7..9fa0a0e41 100644 --- a/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java +++ b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java @@ -4,9 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import org.junit.jupiter.api.Test; import run.endive.redline.experimental.api.NativeMachineFactoryProvider; +import run.endive.redline.experimental.api.internal.RedlineTarget; class RedlinePanamaE2eTest { @@ -32,6 +34,10 @@ public void nativeBuilderProducesCorrectResults() { @Test public void nativeCodeIsAvailable() { + assumeTrue( + RedlineTarget.detectHost().isPresent(), + "Host is not one of the Redline target platforms, so no native code was" + + " cross-compiled for it"); assertNotNull( AddModule.loadNativeCode(), "Native code should be available on this platform"); } diff --git a/redline/it/src/it/redline-e2e/pom.xml b/redline/it/src/it/redline-e2e/pom.xml index 09c52c649..628f2c420 100644 --- a/redline/it/src/it/redline-e2e/pom.xml +++ b/redline/it/src/it/redline-e2e/pom.xml @@ -8,21 +8,13 @@ jar - - run.endive - redline-api-experimental - @project.version@ - + run.endive redline-runner-jffi-experimental @project.version@ - - run.endive - runtime - @project.version@ - org.junit.jupiter @@ -36,6 +28,7 @@ @junit.version@ test + @@ -61,7 +54,6 @@ endive.test.AddModule src/test/resources/add.wat.wasm - WARN true diff --git a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java index baf2dd4f9..039d72e48 100644 --- a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java +++ b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java @@ -4,9 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import org.junit.jupiter.api.Test; import run.endive.redline.experimental.api.NativeMachineFactoryProvider; +import run.endive.redline.experimental.api.internal.RedlineTarget; class RedlineE2eTest { @@ -45,6 +47,10 @@ public void safeBuilderProducesCorrectResults() { @Test public void nativeCodeIsAvailable() { + assumeTrue( + RedlineTarget.detectHost().isPresent(), + "Host is not one of the Redline target platforms, so no native code was" + + " cross-compiled for it"); assertNotNull( AddModule.loadNativeCode(), "Native code should be available on this platform"); } From 04bbc9c9b32407232d10a886026cb7e35c2e669b Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 24 Aug 2026 12:02:13 +0100 Subject: [PATCH 11/20] Fix wasm-publish: correct tag and push directory Two bugs that would have made the publish automation a no-op or produced an unpullable artifact: - It pushed :latest while the bridge POM consumes :999.0.0-SNAPSHOT, so republishing would never update what the build actually reads. The tag now lives in one env var, noted as needing to match the POM. - It pushed from the repo root, so the image title annotation became "redline/cranelift_bridge.wasm" and the OCI client tried to write into a directory that does not exist on pull. Push from redline/ instead. Also emit the wkg.lock refresh command in the job summary, since a publish invalidates the pinned digest until the lock file is updated. --- .github/workflows/wasm-publish.yaml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wasm-publish.yaml b/.github/workflows/wasm-publish.yaml index b95b8e27b..92ac0458f 100644 --- a/.github/workflows/wasm-publish.yaml +++ b/.github/workflows/wasm-publish.yaml @@ -6,6 +6,10 @@ on: paths: ['redline/wasm-build/**'] workflow_dispatch: +# Must stay in sync with the imageRef tag in redline/bridge/pom.xml. +env: + WASM_VERSION: 999.0.0-SNAPSHOT + jobs: build-and-publish: runs-on: ubuntu-latest @@ -32,7 +36,23 @@ jobs: - name: Login to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin + # Pushed from inside redline/ so the org.opencontainers.image.title + # annotation is a bare filename. A path like "redline/cranelift_bridge.wasm" + # makes the OCI client try to write into a directory that does not exist + # on pull. + # + # The tag must be valid semver: the wkg lock file rejects "latest". - name: Push to GHCR + working-directory: redline + run: | + oras push \ + ghcr.io/bytecodealliance/endive-cranelift-bridge:${{ env.WASM_VERSION }} \ + cranelift_bridge.wasm:application/wasm + + - name: Report new digest run: | - oras push ghcr.io/bytecodealliance/endive-cranelift-bridge:latest \ - redline/cranelift_bridge.wasm:application/wasm + echo "Published ${{ env.WASM_VERSION }}. Refresh the pinned digest with:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo './mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo 'then commit redline/wkg.lock.' >> $GITHUB_STEP_SUMMARY From 5e726f39583b55e3d22637a86664222fa6206bbd Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 24 Aug 2026 12:15:16 +0100 Subject: [PATCH 12/20] Fix release.yaml corrupted by the -Predline removal Removing " -Predline" also consumed the following newline in three places, concatenating commands: "versions:set ... git add ." would run git as a Maven lifecycle phase, and "clean deploy" stopped being its own command. One -Predline also survived, referencing a profile this PR deletes. The release workflow could not have succeeded. --- .github/workflows/release.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dccc0f033..a190059cc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -45,6 +45,7 @@ jobs: - name: Compile run: ./mvnw --batch-mode -Dquickly + - name: Setup Git run: | git config user.name "Endive BOT" @@ -52,7 +53,8 @@ jobs: - name: Set the version run: | - ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} git add . + ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} + git add . git commit -m "Release version update ${{ github.event.inputs.release-version }}" git push git tag ${{ github.event.inputs.release-version }} @@ -63,7 +65,8 @@ jobs: - name: Release to Maven Central run: | # -Dquickly is needed to locally publish wasm-corpus - ./mvnw --batch-mode -Dquickly ./mvnw --batch-mode clean deploy -Drelease -Predline -DskipTests=true -X + ./mvnw --batch-mode -Dquickly + ./mvnw --batch-mode clean deploy -Drelease -DskipTests=true -X env: MAVEN_USERNAME: ${{ secrets.SONATYPE_USERNAME }} MAVEN_CENTRAL_TOKEN: ${{ secrets.SONATYPE_PASSWORD }} @@ -71,7 +74,8 @@ jobs: - name: Back to Snapshot run: | - ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT git add . + ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT + git add . git commit -m "Snapshot version update" git push env: From 0d3526c7cc4f25b082e4bd290ed55eb02b273a56 Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 24 Aug 2026 12:20:05 +0100 Subject: [PATCH 13/20] Do not report unknown architectures as x86_64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detectHost() used isAarch64 as its only discriminator, so every other architecture fell through to the x86_64 variant. On Linux/riscv64 (or ppc64le, s390x, 32-bit ARM) it returned LINUX_X86_64, the .x86_64-linux.native resource loaded, and the runner handed x86-64 machine code to the CPU — a JVM crash rather than a fallback to the build-time compiler. Recognise x86_64/amd64/x64 explicitly and return empty otherwise, which routes those platforms to the compiled-bytecode path. Adds the first tests for RedlineTarget, using the endive.redline.os.* overrides. --- redline/api/pom.xml | 10 +++ .../api/internal/RedlineTarget.java | 9 +++ .../api/internal/RedlineTargetTest.java | 66 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 redline/api/src/test/java/run/endive/redline/experimental/api/internal/RedlineTargetTest.java diff --git a/redline/api/pom.xml b/redline/api/pom.xml index 05068e3b2..3b0717413 100644 --- a/redline/api/pom.xml +++ b/redline/api/pom.xml @@ -22,5 +22,15 @@ run.endive wasm + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java index dba0b0ab9..42f0f5f54 100644 --- a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java @@ -45,6 +45,15 @@ public static Optional detectHost() { .toLowerCase(Locale.ROOT); boolean isAarch64 = arch.equals("aarch64") || arch.equals("arm64"); + boolean isX8664 = arch.equals("x86_64") || arch.equals("amd64") || arch.equals("x64"); + + // An unrecognised architecture must yield empty rather than defaulting to + // x86_64: the caller uses this to pick a native code blob, and handing + // x86_64 machine code to, say, riscv64 crashes the JVM instead of falling + // back to the build-time compiler. + if (!isAarch64 && !isX8664) { + return Optional.empty(); + } if (osName.contains("linux")) { return Optional.of(isAarch64 ? LINUX_AARCH64 : LINUX_X86_64); diff --git a/redline/api/src/test/java/run/endive/redline/experimental/api/internal/RedlineTargetTest.java b/redline/api/src/test/java/run/endive/redline/experimental/api/internal/RedlineTargetTest.java new file mode 100644 index 000000000..6ad85bd90 --- /dev/null +++ b/redline/api/src/test/java/run/endive/redline/experimental/api/internal/RedlineTargetTest.java @@ -0,0 +1,66 @@ +package run.endive.redline.experimental.api.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class RedlineTargetTest { + + private static final String OS_NAME = "endive.redline.os.name"; + private static final String OS_ARCH = "endive.redline.os.arch"; + + @AfterEach + public void clearOverrides() { + System.clearProperty(OS_NAME); + System.clearProperty(OS_ARCH); + } + + private static Optional detect(String osName, String arch) { + System.setProperty(OS_NAME, osName); + System.setProperty(OS_ARCH, arch); + return RedlineTarget.detectHost(); + } + + @Test + public void detectsSupportedPlatforms() { + assertEquals(Optional.of(RedlineTarget.LINUX_X86_64), detect("Linux", "amd64")); + assertEquals(Optional.of(RedlineTarget.LINUX_X86_64), detect("Linux", "x86_64")); + assertEquals(Optional.of(RedlineTarget.LINUX_AARCH64), detect("Linux", "aarch64")); + assertEquals(Optional.of(RedlineTarget.MACOS_AARCH64), detect("Mac OS X", "arm64")); + assertEquals(Optional.of(RedlineTarget.MACOS_X86_64), detect("Mac OS X", "x86_64")); + assertEquals(Optional.of(RedlineTarget.WINDOWS_X86_64), detect("Windows 11", "amd64")); + assertEquals(Optional.of(RedlineTarget.WINDOWS_AARCH64), detect("Windows 11", "aarch64")); + } + + /** + * An unrecognised architecture must not be reported as x86_64. Callers use the + * result to select a native code blob, so guessing wrong hands machine code for + * the wrong ISA to the CPU and crashes the JVM, instead of falling back to the + * build-time compiler. + */ + @Test + public void unknownArchitectureIsNotMistakenForX8664() { + for (String arch : new String[] {"riscv64", "ppc64le", "s390x", "arm", "mips64", ""}) { + assertTrue( + detect("Linux", arch).isEmpty(), + "Linux/" + arch + " must not resolve to an x86_64 target"); + } + } + + @Test + public void unknownOperatingSystemIsUnsupported() { + assertTrue(detect("FreeBSD", "amd64").isEmpty()); + assertTrue(detect("SunOS", "amd64").isEmpty()); + } + + @Test + public void everyTargetRoundTripsThroughItsTriple() { + for (RedlineTarget target : RedlineTarget.values()) { + assertEquals(Optional.of(target), RedlineTarget.fromTriple(target.triple())); + } + assertTrue(RedlineTarget.fromTriple("not-a-real-triple").isEmpty()); + } +} From 3e4ea5b52a7817ab17d8fd3dc23d72422579a70f Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 24 Aug 2026 13:49:50 +0100 Subject: [PATCH 14/20] Harden native code loading and expose the selected backend Correctness: - discover() caught ServiceConfigurationError around the loop body, but ServiceLoader raises it from the iterator, so a for-each let it escape. Wrap next() instead, and also catch LinkageError: the Panama runner is compiled for 25 and fails to link on older JDKs, which is exactly the case the catch was meant to tolerate. - NativeCodeSerializer allocated byte[count][] straight from the file, so a corrupt count became OutOfMemoryError before truncation could be detected. Reject negative counts and lengths and collect incrementally. - A corrupt blob threw UncheckedIOException from a static initializer, which bricks the class for the life of the JVM. Load into a local and leave CODE null instead, so builder() degrades to compiled bytecode. DX: - Generate nativeProvider(), non-empty exactly when builder() takes the native path, and have builder() use it as the single source of truth. This makes the choice observable, and gives modules with imported memories the provider they must create those imports with. - Assert in both E2Es that the native path actually engages. Every other assertion there passes when it silently does not, because the fallback produces identical results. Cleanup: drop the unused Cli.execute overload, put expected before actual in the E2E assertEquals calls, and rename the generator's interpreterBuilder helper to compiledBuilder, which is what it emits. --- .../endive/experimental/compiler/cli/Cli.java | 6 +- .../api/NativeCodeSerializer.java | 44 ++-- .../api/NativeMachineFactoryProvider.java | 24 ++- .../experimental/build/RedlineGenerator.java | 201 +++++++++++------- .../endive/test/RedlinePanamaE2eTest.java | 19 +- .../test/java/endive/test/RedlineE2eTest.java | 21 +- 6 files changed, 208 insertions(+), 107 deletions(-) diff --git a/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java b/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java index 6a954e005..a97b23a8b 100644 --- a/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java +++ b/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java @@ -107,11 +107,7 @@ public void run() { } } - public static int execute(String[] args) { - return new CommandLine(new Cli()).execute(args); - } - public static void main(String[] args) { - System.exit(execute(args)); + System.exit(new CommandLine(new Cli()).execute(args)); } } diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java index f4cf3cb87..3267aa619 100644 --- a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java @@ -5,6 +5,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; /** * Serializes/deserializes pre-compiled native code (byte[][]). @@ -54,22 +56,38 @@ public static byte[][] deserialize(InputStream in) throws IOException { throw new IOException("Unsupported native code version: " + version); } int count = dis.readInt(); - byte[][] code = new byte[count][]; + if (count < 0) { + throw new IOException("Invalid native code file: negative function count " + count); + } + // Collected rather than pre-allocated: a corrupt count would otherwise + // reserve up to 2^31 array slots before any read could reveal the file is + // truncated, turning a bad file into an OutOfMemoryError. + List code = new ArrayList<>(Math.min(count, 1024)); for (int i = 0; i < count; i++) { int len = dis.readInt(); - if (len > 0) { - code[i] = dis.readNBytes(len); - if (code[i].length != len) { - throw new IOException( - "Truncated native code for function " - + i - + ": expected " - + len - + " bytes, got " - + code[i].length); - } + if (len < 0) { + throw new IOException( + "Invalid native code file: negative code length " + + len + + " for function " + + i); + } + if (len == 0) { + code.add(null); + continue; + } + byte[] func = dis.readNBytes(len); + if (func.length != len) { + throw new IOException( + "Truncated native code for function " + + i + + ": expected " + + len + + " bytes, got " + + func.length); } + code.add(func); } - return code; + return code.toArray(new byte[0][]); } } diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeMachineFactoryProvider.java b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeMachineFactoryProvider.java index 2ff6f2b27..c3d85ae34 100644 --- a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeMachineFactoryProvider.java +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeMachineFactoryProvider.java @@ -22,14 +22,24 @@ public interface NativeMachineFactoryProvider { static Optional discover() { NativeMachineFactoryProvider best = null; - var loader = ServiceLoader.load(NativeMachineFactoryProvider.class); - for (var provider : loader) { + var it = ServiceLoader.load(NativeMachineFactoryProvider.class).iterator(); + while (it.hasNext()) { + NativeMachineFactoryProvider provider; try { - if (best == null || provider.priority() > best.priority()) { - best = provider; - } - } catch (ServiceConfigurationError e) { - // Provider can't load on this JDK (e.g., Panama on JDK < 25) — skip + provider = it.next(); + } catch (ServiceConfigurationError | LinkageError e) { + // This provider cannot be loaded on this JDK — the Panama runner is + // compiled for 25, so instantiating it on an older JDK fails here. + // Skip it and let a lower-priority provider win. + // + // The catch must wrap next(): ServiceLoader reports these failures + // from the iterator, not from anything we do with the provider, so a + // for-each loop would let them escape. next() has already advanced + // past the failed provider, so this cannot spin. + continue; + } + if (best == null || provider.priority() > best.priority()) { + best = provider; } } return Optional.ofNullable(best); diff --git a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java index 6e680b4c3..4e6357fd4 100644 --- a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java +++ b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java @@ -12,13 +12,11 @@ import com.github.javaparser.ast.expr.AssignExpr; import com.github.javaparser.ast.expr.BinaryExpr; import com.github.javaparser.ast.expr.ClassExpr; -import com.github.javaparser.ast.expr.ConditionalExpr; import com.github.javaparser.ast.expr.FieldAccessExpr; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.MethodReferenceExpr; import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.expr.NullLiteralExpr; -import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.expr.StringLiteralExpr; import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.stmt.BlockStmt; @@ -26,7 +24,6 @@ import com.github.javaparser.ast.stmt.ExpressionStmt; import com.github.javaparser.ast.stmt.IfStmt; import com.github.javaparser.ast.stmt.ReturnStmt; -import com.github.javaparser.ast.stmt.ThrowStmt; import com.github.javaparser.ast.stmt.TryStmt; import com.github.javaparser.ast.type.VarType; import java.io.FileOutputStream; @@ -99,11 +96,12 @@ public void extendGeneratedSources() throws IOException { cu.addImport("run.endive.redline.experimental.api.internal.RedlineTarget"); cu.addImport("java.io.InputStream"); cu.addImport("java.io.IOException"); - cu.addImport("java.io.UncheckedIOException"); + cu.addImport("java.util.Optional"); cu.addImport("run.endive.runtime.Instance"); generateNativeCodeHolderInnerClass(type, baseName); generateLoadNativeCodeMethod(type); + generateNativeProviderMethod(type); generateBuilderMethod(type, baseName); generateSafeBuilderMethod(type, baseName); @@ -117,21 +115,28 @@ private static void generateNativeCodeHolderInnerClass( // private static class NativeCodeHolder { // static final byte[][] CODE; // static { + // byte[][] loaded = null; // var host = RedlineTarget.detectHost().orElse(null); - // if (host == null) { - // CODE = null; - // } else { + // if (host != null) { // String resource = "." + host.resourceSuffix() + ".native"; // try (InputStream in = // .class.getResourceAsStream(resource)) { - // CODE = (in == null) ? null : NativeCodeSerializer.deserialize(in); + // if (in != null) { + // loaded = NativeCodeSerializer.deserialize(in); + // } // } catch (IOException e) { - // throw new UncheckedIOException("Failed to load native code", e); + // loaded = null; // } // } + // CODE = loaded; // } // } // + // + // Loading into a local keeps CODE definitely-assigned-once (a static final + // cannot be assigned in both the try and the catch), and lets a missing or + // unreadable blob leave CODE null so builder() falls back to the build-time + // compiled bytecode instead of the class being permanently unusable. var holderClass = new ClassOrInterfaceDeclaration( NodeList.nodeList( @@ -144,6 +149,13 @@ private static void generateNativeCodeHolderInnerClass( holderClass.addField( parseType("byte[][]"), "CODE", Modifier.Keyword.STATIC, Modifier.Keyword.FINAL); + // byte[][] loaded = null; + var loadedVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + parseType("byte[][]"), "loaded", new NullLiteralExpr()))); + // var host = RedlineTarget.detectHost().orElse(null); var detectHost = new MethodCallExpr( @@ -155,14 +167,6 @@ private static void generateNativeCodeHolderInnerClass( new VariableDeclarationExpr( new VariableDeclarator(new VarType(), "host", detectHost))); - // CODE = null; - var assignNull = - new ExpressionStmt( - new AssignExpr( - new NameExpr("CODE"), - new NullLiteralExpr(), - AssignExpr.Operator.ASSIGN)); - // String resource = "." + host.resourceSuffix() + ".native"; var resourceName = new BinaryExpr( @@ -190,38 +194,41 @@ private static void generateNativeCodeHolderInnerClass( new VariableDeclarationExpr( new VariableDeclarator(parseType("InputStream"), "in", getResource)); - // CODE = (in == null) ? null : NativeCodeSerializer.deserialize(in); - var deserialize = - new ConditionalExpr( - new BinaryExpr( - new NameExpr("in"), - new NullLiteralExpr(), - BinaryExpr.Operator.EQUALS), - new NullLiteralExpr(), - new MethodCallExpr( - new NameExpr("NativeCodeSerializer"), - "deserialize", - new NodeList<>(new NameExpr("in")))); - var assignCode = + // if (in != null) { loaded = NativeCodeSerializer.deserialize(in); } + var assignLoaded = + new ExpressionStmt( + new AssignExpr( + new NameExpr("loaded"), + new MethodCallExpr( + new NameExpr("NativeCodeSerializer"), + "deserialize", + new NodeList<>(new NameExpr("in"))), + AssignExpr.Operator.ASSIGN)); + var ifStreamPresent = + new IfStmt() + .setCondition( + new BinaryExpr( + new NameExpr("in"), + new NullLiteralExpr(), + BinaryExpr.Operator.NOT_EQUALS)) + .setThenStmt(new BlockStmt(new NodeList<>(assignLoaded))); + + // catch (IOException e) { loaded = null; } + var resetLoaded = new ExpressionStmt( new AssignExpr( - new NameExpr("CODE"), deserialize, AssignExpr.Operator.ASSIGN)); - - // catch (IOException e) { throw new UncheckedIOException("...", e); } - var newException = - new ObjectCreationExpr() - .setType(parseClassOrInterfaceType("UncheckedIOException")) - .addArgument(new StringLiteralExpr("Failed to load native code")) - .addArgument(new NameExpr("e")); + new NameExpr("loaded"), + new NullLiteralExpr(), + AssignExpr.Operator.ASSIGN)); var catchIoException = new CatchClause() .setParameter(new Parameter(parseClassOrInterfaceType("IOException"), "e")) - .setBody(new BlockStmt(new NodeList<>(new ThrowStmt(newException)))); + .setBody(new BlockStmt(new NodeList<>(resetLoaded))); var tryLoad = new TryStmt() .setResources(new NodeList<>(streamResource)) - .setTryBlock(new BlockStmt(new NodeList<>(assignCode))) + .setTryBlock(new BlockStmt(new NodeList<>(ifStreamPresent))) .setCatchClauses(new NodeList<>(catchIoException)); var loadFromResource = @@ -230,13 +237,22 @@ private static void generateNativeCodeHolderInnerClass( new BinaryExpr( new NameExpr("host"), new NullLiteralExpr(), - BinaryExpr.Operator.EQUALS)) - .setThenStmt(new BlockStmt(new NodeList<>(assignNull))) - .setElseStmt(new BlockStmt(new NodeList<>(resourceVar, tryLoad))); + BinaryExpr.Operator.NOT_EQUALS)) + .setThenStmt(new BlockStmt(new NodeList<>(resourceVar, tryLoad))); + + // CODE = loaded; + var assignCode = + new ExpressionStmt( + new AssignExpr( + new NameExpr("CODE"), + new NameExpr("loaded"), + AssignExpr.Operator.ASSIGN)); var initBody = holderClass.addStaticInitializer(); + initBody.addStatement(loadedVar); initBody.addStatement(hostVar); initBody.addStatement(loadFromResource); + initBody.addStatement(assignCode); } private static void generateLoadNativeCodeMethod(ClassOrInterfaceDeclaration type) { @@ -255,21 +271,61 @@ private static void generateLoadNativeCodeMethod(ClassOrInterfaceDeclaration typ new FieldAccessExpr(new NameExpr("NativeCodeHolder"), "CODE"))); } + private static void generateNativeProviderMethod(ClassOrInterfaceDeclaration type) { + // Generates: + // + // public static Optional<NativeMachineFactoryProvider> nativeProvider() { + // if (loadNativeCode() == null) { + // return Optional.empty(); + // } + // return NativeMachineFactoryProvider.discover(); + // } + // + // + // Non-empty exactly when builder() will take the native path, so callers can + // tell which backend they got. Modules with imported memories or tables need + // this: the native machines reject a memory that was not created by their own + // factory, so imports must be built with the returned provider. + var method = + type.addMethod("nativeProvider", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) + .setType( + parseClassOrInterfaceType( + "Optional")); + + var returnEmpty = new ReturnStmt(new MethodCallExpr(new NameExpr("Optional"), "empty")); + var ifNoNativeCode = + new IfStmt() + .setCondition( + new BinaryExpr( + new MethodCallExpr("loadNativeCode"), + new NullLiteralExpr(), + BinaryExpr.Operator.EQUALS)) + .setThenStmt(new BlockStmt(new NodeList<>(returnEmpty))); + + var body = method.createBody(); + body.addStatement(ifNoNativeCode); + body.addStatement( + new ReturnStmt( + new MethodCallExpr( + new NameExpr("NativeMachineFactoryProvider"), "discover"))); + } + private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, String moduleName) { // Generates: // // public static Instance.Builder builder() { // var module = load(); - // byte[][] nativeCode = loadNativeCode(); - // if (nativeCode != null) { - // var provider = NativeMachineFactoryProvider.discover(); - // if (provider.isPresent()) { - // return provider.get().builder(module, nativeCode); - // } + // var provider = nativeProvider(); + // if (provider.isPresent()) { + // return provider.get().builder(module, loadNativeCode()); // } // return Instance.builder(module).withMachineFactory(::create); // } // + // + // The native path is selected through nativeProvider() so that callers + // checking it see exactly the decision this method makes. Falling back means + // the build-time compiled bytecode, not the interpreter. var method = type.addMethod("builder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) .setType(parseClassOrInterfaceType("Instance.Builder")); @@ -281,54 +337,35 @@ private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, Stri new VariableDeclarator( new VarType(), "module", new MethodCallExpr("load")))); - // byte[][] nativeCode = loadNativeCode(); - var nativeCodeVar = - new ExpressionStmt( - new VariableDeclarationExpr( - new VariableDeclarator( - parseType("byte[][]"), - "nativeCode", - new MethodCallExpr("loadNativeCode")))); - - // var provider = NativeMachineFactoryProvider.discover(); + // var provider = nativeProvider(); var providerVar = new ExpressionStmt( new VariableDeclarationExpr( new VariableDeclarator( new VarType(), "provider", - new MethodCallExpr( - new NameExpr("NativeMachineFactoryProvider"), - "discover")))); + new MethodCallExpr("nativeProvider")))); - // return provider.get().builder(module, nativeCode); + // return provider.get().builder(module, loadNativeCode()); var returnNative = new ReturnStmt( new MethodCallExpr( new MethodCallExpr(new NameExpr("provider"), "get"), "builder", new NodeList<>( - new NameExpr("module"), new NameExpr("nativeCode")))); + new NameExpr("module"), + new MethodCallExpr("loadNativeCode")))); var ifProviderPresent = new IfStmt() .setCondition(new MethodCallExpr(new NameExpr("provider"), "isPresent")) .setThenStmt(new BlockStmt(new NodeList<>(returnNative))); - var ifNativeCode = - new IfStmt() - .setCondition( - new BinaryExpr( - new NameExpr("nativeCode"), - new NullLiteralExpr(), - BinaryExpr.Operator.NOT_EQUALS)) - .setThenStmt(new BlockStmt(new NodeList<>(providerVar, ifProviderPresent))); - var body = method.createBody(); body.addStatement(moduleVar); - body.addStatement(nativeCodeVar); - body.addStatement(ifNativeCode); - body.addStatement(new ReturnStmt(interpreterBuilder(new NameExpr("module"), moduleName))); + body.addStatement(providerVar); + body.addStatement(ifProviderPresent); + body.addStatement(new ReturnStmt(compiledBuilder(new NameExpr("module"), moduleName))); } private static void generateSafeBuilderMethod( @@ -345,11 +382,17 @@ private static void generateSafeBuilderMethod( method.createBody() .addStatement( - new ReturnStmt(interpreterBuilder(new MethodCallExpr("load"), moduleName))); + new ReturnStmt(compiledBuilder(new MethodCallExpr("load"), moduleName))); } - /** {@code Instance.builder().withMachineFactory(::create)} */ - private static MethodCallExpr interpreterBuilder( + /** + * {@code Instance.builder().withMachineFactory(::create)} + * + *

{@code create} returns the machine the build-time compiler emitted as JVM + * bytecode, so this is the compiled path, not the interpreter. The interpreter is + * only reached per-function, for functions too large to fit a JVM method. + */ + private static MethodCallExpr compiledBuilder( com.github.javaparser.ast.expr.Expression module, String moduleName) { return new MethodCallExpr( new MethodCallExpr(new NameExpr("Instance"), "builder", new NodeList<>(module)), diff --git a/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java index 9fa0a0e41..7a9074a6b 100644 --- a/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java +++ b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java @@ -19,6 +19,23 @@ public void panamaProviderIsSelected() { assertEquals(100, provider.get().priority(), "Panama should win with priority 100"); } + /** + * Without this, every other test here still passes when the native path silently + * never engages, because the fallback is the build-time compiled bytecode and + * produces identical results. + */ + @Test + public void builderActuallyUsesTheNativePath() { + assumeTrue( + RedlineTarget.detectHost().isPresent(), + "Host is not one of the Redline target platforms"); + var provider = AddModule.nativeProvider(); + assertTrue( + provider.isPresent(), + "builder() must take the native path, not fall back to compiled bytecode"); + assertEquals(100, provider.get().priority(), "and it must be the Panama runner"); + } + @Test public void nativeBuilderProducesCorrectResults() { try (var instance = AddModule.builder().build()) { @@ -26,8 +43,8 @@ public void nativeBuilderProducesCorrectResults() { assertArrayEquals(new long[] {3}, add.apply(1, 2)); assertArrayEquals(new long[] {0}, add.apply(0, 0)); assertEquals( - (int) add.apply(0, -1)[0], -1, + (int) add.apply(0, -1)[0], "i32 add(0, -1) should be -1 when narrowed to int"); } } diff --git a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java index 039d72e48..31dff4161 100644 --- a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java +++ b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java @@ -19,6 +19,23 @@ public void jffiProviderIsSelected() { assertEquals(50, provider.get().priority(), "JFFI should be selected with priority 50"); } + /** + * Without this, every other test here still passes when the native path silently + * never engages, because the fallback is the build-time compiled bytecode and + * produces identical results. + */ + @Test + public void builderActuallyUsesTheNativePath() { + assumeTrue( + RedlineTarget.detectHost().isPresent(), + "Host is not one of the Redline target platforms"); + var provider = AddModule.nativeProvider(); + assertTrue( + provider.isPresent(), + "builder() must take the native path, not fall back to compiled bytecode"); + assertEquals(50, provider.get().priority(), "and it must be the JFFI runner"); + } + @Test public void nativeBuilderProducesCorrectResults() { try (var instance = AddModule.builder().build()) { @@ -26,8 +43,8 @@ public void nativeBuilderProducesCorrectResults() { assertArrayEquals(new long[] {3}, add.apply(1, 2)); assertArrayEquals(new long[] {0}, add.apply(0, 0)); assertEquals( - (int) add.apply(0, -1)[0], -1, + (int) add.apply(0, -1)[0], "i32 add(0, -1) should be -1 when narrowed to int"); } } @@ -39,8 +56,8 @@ public void safeBuilderProducesCorrectResults() { assertArrayEquals(new long[] {3}, add.apply(1, 2)); assertArrayEquals(new long[] {0}, add.apply(0, 0)); assertEquals( - (int) add.apply(0, -1)[0], -1, + (int) add.apply(0, -1)[0], "i32 add(0, -1) should be -1 when narrowed to int"); } } From 8b1713853f00ec9f9dbfcd6e85c0cfdf0a81cac9 Mon Sep 17 00:00:00 2001 From: andreatp Date: Mon, 24 Aug 2026 15:59:58 +0100 Subject: [PATCH 15/20] Drop redline.yaml, now redundant with the main CI workflow Since redline builds unconditionally and no longer needs -Predline or a Rust toolchain, ci.yaml runs the identical command on a superset of the platforms: it covers ubuntu-latest/25 and macos-latest/25 (exactly the redline matrix), pins the same testsuite ref and sets the same MAVEN_OPTS. Note this leaves redline covered only by jobs with continue-on-error, so redline failures no longer turn the build red on their own. --- .github/workflows/redline.yaml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/workflows/redline.yaml diff --git a/.github/workflows/redline.yaml b/.github/workflows/redline.yaml deleted file mode 100644 index 961a7861b..000000000 --- a/.github/workflows/redline.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: Redline CI - -on: - push: - branches: [ main ] - pull_request: - -jobs: - redline: - name: Redline (Java 25, ${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - steps: - - name: Checkout sources - uses: actions/checkout@v7 - - name: Checkout testsuite - uses: actions/checkout@v7 - with: - repository: WebAssembly/testsuite - path: testsuite - ref: 88e97b0f742f4c3ee01fea683da130f344dd7b02 - - name: Set up Java - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '25' - cache: maven - - name: Build and test redline - run: ./mvnw -B clean install - env: - MAVEN_OPTS: "-ea" From df1525375257d69faad692020817688e8f6e0834 Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 25 Aug 2026 11:05:55 +0100 Subject: [PATCH 16/20] Document the Cranelift bridge wasm workflow in CONTRIBUTING None of this was written down: that a Rust toolchain is not needed because inlay fetches a digest-pinned wasm from GHCR, how to build the bridge locally, that a stale local wasm silently shadows the pinned one, and that publishing does not refresh wkg.lock so the lock has to be updated in a follow-up commit. --- CONTRIBUTING.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5464aa124..ce2cdac59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,6 +86,30 @@ Basic steps: note: if you're working using a *corporate proxy* (or anything like this), you might need to pass the usual `-Dhttps.proxyHost=...` and `-Dhttps.proxyPort=...` in order to properly instruct Maven about this (this can be required for example for `test-gen-plugin` since it downloads the testsuite). +### Redline and the Cranelift bridge + +The experimental redline native compiler needs `cranelift_bridge.wasm`, a Rust crate compiled to `wasm32-wasip1`. **You do not need a Rust toolchain to build Endive.** The [inlay](https://github.com/roastedroot/inlay) Maven plugin downloads a prebuilt copy from GHCR during `generate-sources`, pinned by digest in `redline/wkg.lock`, so a fresh clone builds with a plain `mvn clean install`. + +To work on the Rust side you do need Rust with the `wasm32-wasip1` target: + +* `make -C redline/wasm-build all` builds `redline/cranelift_bridge.wasm` (gitignored) +* inlay skips the download whenever that file already exists, so your local build picks it up + +That skip has a sharp edge: a **stale** `redline/cranelift_bridge.wasm` left over from an earlier `make all` silently shadows the pinned artifact, and you end up testing against a different bridge than CI. Delete the file to go back to the published one. + +Publishing is handled by `.github/workflows/wasm-publish.yaml`, which runs on pushes to `main` touching `redline/wasm-build/**`, or on manual dispatch, and pushes to `ghcr.io/bytecodealliance/endive-cranelift-bridge`. Two constraints are easy to trip over: + +* the tag must be valid semver — the `wkg.lock` format rejects `latest` +* `WASM_VERSION` in that workflow must match the `imageRef` in `redline/bridge/pom.xml`, otherwise you publish something no build consumes + +Publishing does **not** update the lock file. Until it is refreshed, builds keep resolving the previously pinned digest, and if the same tag was re-pushed inlay fails with a digest mismatch instead of silently drifting. Refresh it with: + +```bash +./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update +``` + +then commit the updated `redline/wkg.lock`. + ### Proposals implementation Our priority is to focus on implementing [proposals](https://github.com/WebAssembly/proposals) that are in the most advanced stages of development. While we wholeheartedly encourage and support explorations, we’ll be dedicating less time to early-stage proposals until we have more comprehensive support for those that are stabilized. From 36d1291e5250054e00574db3a94c361d92f6e00b Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 25 Aug 2026 11:35:27 +0100 Subject: [PATCH 17/20] Make the bridge wasm tag a property, settable from the publish workflow The imageRef was a hardcoded 999.0.0-SNAPSHOT, which versions:set does not rewrite, so a release would have shipped jars built from a mutable OCI tag: once that tag moved, the release could no longer be rebuilt from its git tag. The tag is now the cranelift-bridge.version property, and the publish workflow takes a version as a workflow_dispatch input, so an immutable release tag can be published and adopted before cutting a release. The workflow validates the input is semver up front, since an unparseable tag otherwise only fails later when a consumer writes wkg.lock. ${project.version} cannot be reused here: the Maven version 999-SNAPSHOT is not semver, which is what wkg.lock requires. --- .github/workflows/wasm-publish.yaml | 55 +++++++++++++++++++++++------ CONTRIBUTING.md | 16 ++++++--- pom.xml | 9 +++++ redline/bridge/pom.xml | 2 +- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/.github/workflows/wasm-publish.yaml b/.github/workflows/wasm-publish.yaml index 92ac0458f..6e604f763 100644 --- a/.github/workflows/wasm-publish.yaml +++ b/.github/workflows/wasm-publish.yaml @@ -5,10 +5,20 @@ on: branches: [main] paths: ['redline/wasm-build/**'] workflow_dispatch: + inputs: + version: + description: >- + Semver tag to publish, e.g. 1.2.0. Use an immutable release tag when + preparing a release; leave the default to refresh the development + snapshot. Must be valid semver: wkg.lock rejects tags like "latest". + required: true + default: 999.0.0-SNAPSHOT -# Must stay in sync with the imageRef tag in redline/bridge/pom.xml. +# Pushes to main refresh the development snapshot; a manual run can publish any +# semver tag. Must match cranelift-bridge.version in the root pom.xml for the +# build to actually consume what was published. env: - WASM_VERSION: 999.0.0-SNAPSHOT + WASM_VERSION: ${{ inputs.version || '999.0.0-SNAPSHOT' }} jobs: build-and-publish: @@ -21,6 +31,15 @@ jobs: - name: Checkout sources uses: actions/checkout@v7 + # Catches "latest", "1.2" and similar before anything is pushed: an + # unparseable tag only fails later, when a consumer writes wkg.lock. + - name: Validate version is semver + run: | + if ! echo "$WASM_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then + echo "::error::'$WASM_VERSION' is not valid semver; wkg.lock would reject it" + exit 1 + fi + - name: Set up Rust uses: dtolnay/rust-toolchain@stable with: @@ -40,19 +59,33 @@ jobs: # annotation is a bare filename. A path like "redline/cranelift_bridge.wasm" # makes the OCI client try to write into a directory that does not exist # on pull. - # - # The tag must be valid semver: the wkg lock file rejects "latest". - name: Push to GHCR working-directory: redline run: | oras push \ - ghcr.io/bytecodealliance/endive-cranelift-bridge:${{ env.WASM_VERSION }} \ + ghcr.io/bytecodealliance/endive-cranelift-bridge:${WASM_VERSION} \ cranelift_bridge.wasm:application/wasm - - name: Report new digest + - name: Next steps run: | - echo "Published ${{ env.WASM_VERSION }}. Refresh the pinned digest with:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo './mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo 'then commit redline/wkg.lock.' >> $GITHUB_STEP_SUMMARY + { + echo "Published \`$WASM_VERSION\`." + echo + echo "Publishing does not update the lock file. Until it is refreshed the" + echo "build keeps resolving the previously pinned digest, and re-pushing an" + echo "already-locked tag makes every build fail with a digest mismatch." + echo + echo "To consume it:" + echo + echo '```bash' + echo "# 1. point the build at this tag" + echo "./mvnw versions:set-property -Dproperty=cranelift-bridge.version \\" + echo " -DnewVersion=$WASM_VERSION -DgenerateBackupPoms=false" + echo + echo "# 2. re-pin the digest" + echo "./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update" + echo + echo "# 3. commit both" + echo "git commit -am 'Use cranelift_bridge.wasm $WASM_VERSION'" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce2cdac59..3efbcd4d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,18 +97,24 @@ To work on the Rust side you do need Rust with the `wasm32-wasip1` target: That skip has a sharp edge: a **stale** `redline/cranelift_bridge.wasm` left over from an earlier `make all` silently shadows the pinned artifact, and you end up testing against a different bridge than CI. Delete the file to go back to the published one. -Publishing is handled by `.github/workflows/wasm-publish.yaml`, which runs on pushes to `main` touching `redline/wasm-build/**`, or on manual dispatch, and pushes to `ghcr.io/bytecodealliance/endive-cranelift-bridge`. Two constraints are easy to trip over: +Publishing is handled by `.github/workflows/wasm-publish.yaml`. Pushes to `main` touching `redline/wasm-build/**` refresh the development snapshot; a manual dispatch can publish any semver tag. The tag must be valid semver — the `wkg.lock` format rejects `latest` — and the workflow validates that before pushing anything. -* the tag must be valid semver — the `wkg.lock` format rejects `latest` -* `WASM_VERSION` in that workflow must match the `imageRef` in `redline/bridge/pom.xml`, otherwise you publish something no build consumes +Which tag the build consumes is the `cranelift-bridge.version` property in the root `pom.xml`. Publishing does **not** update it, and does **not** refresh the lock file: until both are updated the build keeps resolving the previously pinned digest, and re-pushing an already-locked tag makes every build fail with a digest mismatch rather than silently drifting. -Publishing does **not** update the lock file. Until it is refreshed, builds keep resolving the previously pinned digest, and if the same tag was re-pushed inlay fails with a digest mismatch instead of silently drifting. Refresh it with: +To adopt a published wasm (the workflow prints these in its job summary): ```bash +# 1. point the build at the tag that was published +./mvnw versions:set-property -Dproperty=cranelift-bridge.version \ + -DnewVersion= -DgenerateBackupPoms=false + +# 2. re-pin the digest ./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update + +# 3. commit pom.xml and redline/wkg.lock together ``` -then commit the updated `redline/wkg.lock`. +**Before cutting a release**, publish an immutable release tag (say `1.2.0`) via workflow dispatch and adopt it with the steps above. Releasing while the property still points at `999.0.0-SNAPSHOT` produces jars built from a mutable tag: once that tag is re-pushed the release can no longer be rebuilt from its git tag, because the pinned digest no longer resolves. ### Proposals implementation diff --git a/pom.xml b/pom.xml index 496719378..466fac6ab 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,15 @@ 31.0.0 9.10.1 + + 999.0.0-SNAPSHOT 2.22.0 3.20.0 2.22 diff --git a/redline/bridge/pom.xml b/redline/bridge/pom.xml index eb32af6c7..2381e3efa 100644 --- a/redline/bridge/pom.xml +++ b/redline/bridge/pom.xml @@ -46,7 +46,7 @@ - ghcr.io/bytecodealliance/endive-cranelift-bridge:999.0.0-SNAPSHOT + ghcr.io/bytecodealliance/endive-cranelift-bridge:${cranelift-bridge.version} ${project.basedir}/../cranelift_bridge.wasm From cd105da97cf677c7e5fcd8b6995d62ed3f6c1446 Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 25 Aug 2026 11:47:51 +0100 Subject: [PATCH 18/20] Publish an immutable bridge wasm as part of the release The adopt step was manual, so forgetting it would silently ship jars built against the mutable development snapshot, unreproducible as soon as that tag moved. The release now retags the digest already pinned in wkg.lock under the release version, points cranelift-bridge.version at it and re-pins the lock, all before the version-bump commit so both land in it. Retagging rather than rebuilding means the released wasm is byte-identical to the artifact CI tested and no Rust toolchain is needed in this pipeline. A guard before deploy refuses to publish if the property still resolves to a SNAPSHOT tag, and the snapshot value is restored afterwards so main is not left pinned to the release tag. --- .github/workflows/release.yaml | 49 ++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a190059cc..d5b8eac23 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -51,6 +51,38 @@ jobs: git config user.name "Endive BOT" git config user.email "endive@bytecodealliance.org" + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + # Jars must be built against an immutable wasm tag: the development + # snapshot is mutable, so a release built against it stops being + # reproducible the moment that tag moves. + # + # This retags the digest already pinned in wkg.lock rather than rebuilding + # from Rust, so the released wasm is byte-identical to the one CI tested + # and no toolchain is needed here. + - name: Publish the bridge wasm under the release version + run: | + set -euo pipefail + DIGEST=$(grep -oE 'sha256:[0-9a-f]{64}' redline/wkg.lock | head -1 || true) + if [ -z "$DIGEST" ]; then + echo "::error::No digest found in redline/wkg.lock" + exit 1 + fi + echo "Retagging $DIGEST as $VERSION" + echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin + oras tag "ghcr.io/bytecodealliance/endive-cranelift-bridge@${DIGEST}" "$VERSION" + env: + VERSION: ${{ github.event.inputs.release-version }} + + # Runs before "Set the version" so the property and lock changes are + # swept into the release commit by its "git add ." below. + - name: Pin the build to the released wasm + run: | + ./mvnw versions:set-property -Dproperty=cranelift-bridge.version \ + -DnewVersion=${{ github.event.inputs.release-version }} -DgenerateBackupPoms=false + ./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update + - name: Set the version run: | ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} @@ -62,6 +94,20 @@ jobs: env: GITHUB_TOKEN: ${{secrets.GH_TOKEN}} + # Last line of defence: if the steps above were skipped or edited away, the + # jars would be built from a mutable tag and could not be rebuilt later. + - name: Verify the bridge wasm tag is immutable + run: | + set -euo pipefail + v=$(./mvnw help:evaluate -Dexpression=cranelift-bridge.version -q -DforceStdout) + echo "cranelift-bridge.version = $v" + case "$v" in + *SNAPSHOT*) + echo "::error::Refusing to release jars built against the mutable wasm tag '$v'" + exit 1 + ;; + esac + - name: Release to Maven Central run: | # -Dquickly is needed to locally publish wasm-corpus @@ -75,6 +121,9 @@ jobs: - name: Back to Snapshot run: | ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT + ./mvnw versions:set-property -Dproperty=cranelift-bridge.version \ + -DnewVersion=999.0.0-SNAPSHOT -DgenerateBackupPoms=false + ./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update git add . git commit -m "Snapshot version update" git push diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3efbcd4d5..5d579a655 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,7 +114,7 @@ To adopt a published wasm (the workflow prints these in its job summary): # 3. commit pom.xml and redline/wkg.lock together ``` -**Before cutting a release**, publish an immutable release tag (say `1.2.0`) via workflow dispatch and adopt it with the steps above. Releasing while the property still points at `999.0.0-SNAPSHOT` produces jars built from a mutable tag: once that tag is re-pushed the release can no longer be rebuilt from its git tag, because the pinned digest no longer resolves. +**Releases handle this automatically.** `release.yaml` retags the digest currently pinned in `wkg.lock` as the release version, points the property at it, re-pins the lock, and commits both alongside the version bump — so every release has a matching immutable wasm artifact, byte-identical to the one CI tested. It retags rather than rebuilding, so the release needs no Rust toolchain. Afterwards it restores the snapshot property, and a guard refuses to deploy if the property still resolves to a `SNAPSHOT` tag. ### Proposals implementation From 8b9596888b22adf3a80abeb9f3dd8ce34ceec39f Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 25 Aug 2026 13:19:46 +0100 Subject: [PATCH 19/20] Annotate the published wasm with its source repository Without org.opencontainers.image.source the GHCR package is not linked to this repository, so GITHUB_TOKEN has no write access to it from Actions and the package does not appear under the repo. The package was first pushed by hand and carries no such annotation, so the link has to be granted once in the package settings; this keeps it in place for every subsequent publish. --- .github/workflows/wasm-publish.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/wasm-publish.yaml b/.github/workflows/wasm-publish.yaml index 6e604f763..d101f71ac 100644 --- a/.github/workflows/wasm-publish.yaml +++ b/.github/workflows/wasm-publish.yaml @@ -59,10 +59,15 @@ jobs: # annotation is a bare filename. A path like "redline/cranelift_bridge.wasm" # makes the OCI client try to write into a directory that does not exist # on pull. + # + # The source annotation links the package to this repository, which is what + # lets GITHUB_TOKEN write to it from Actions and makes it show up under the + # repo's packages. - name: Push to GHCR working-directory: redline run: | oras push \ + --annotation "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \ ghcr.io/bytecodealliance/endive-cranelift-bridge:${WASM_VERSION} \ cranelift_bridge.wasm:application/wasm From 2c1eef6a3bf1a77e43dcf02ca15d598e8d0814b3 Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 25 Aug 2026 13:27:53 +0100 Subject: [PATCH 20/20] Keep redline test modules out of the release reactor it, runner-jffi-tests and runner-tests publish nothing, but all three were being built during a release: the first two sat in the unconditional module list and runner-tests rode in via the java25 profile. Building them there only adds time and risk to the release. They now sit behind !release activation, mirroring how the root pom keeps its own test modules out of releases. --- redline/pom.xml | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/redline/pom.xml b/redline/pom.xml index 2c617995f..772ef4d93 100644 --- a/redline/pom.xml +++ b/redline/pom.xml @@ -13,18 +13,34 @@ Endive - Redline Redline native compiler for WebAssembly - + api bridge build-time-compiler compiler - it runner-jffi - runner-jffi-tests + + + default-redline-test-modules + + + !release + + + + it + runner-jffi-tests + + + java25 @@ -32,6 +48,19 @@ runner + + + + + + java25-test-modules + + [25,) + + !release + + + runner-tests