diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index 6001b8346f..9d9933d24e 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -22,21 +22,69 @@ jobs: - name: Check out repository uses: actions/checkout@v4 + - name: Install native build tools + run: | + sudo apt-get update + sudo apt-get install -y clang + + # Install JDKs and export their paths - name: Set up JDK 8 uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '8' - cache: 'maven' + - name: Save JDK 8 Path + run: echo "JDK_8_HOME=$JAVA_HOME" >> $GITHUB_ENV - - name: Install native build tools - run: | - sudo apt-get update - sudo apt-get install -y clang + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + - name: Save JDK 11 Path + run: echo "JDK_11_HOME=$JAVA_HOME" >> $GITHUB_ENV + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: Save JDK 17 Path + run: echo "JDK_17_HOME=$JAVA_HOME" >> $GITHUB_ENV + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Save JDK 21 Path + run: echo "JDK_21_HOME=$JAVA_HOME" >> $GITHUB_ENV + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '25' + - name: Save JDK 25 Path + run: echo "JDK_25_HOME=$JAVA_HOME" >> $GITHUB_ENV + + # Restore JDK 8 as the main runner + - name: Restore JDK 8 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '8' + cache: 'maven' - name: Run ParparVM JVM tests working-directory: vm - run: mvn -B test + run: mvn -B clean test -pl tests -am + env: + JDK_8_HOME: ${{ env.JDK_8_HOME }} + JDK_11_HOME: ${{ env.JDK_11_HOME }} + JDK_17_HOME: ${{ env.JDK_17_HOME }} + JDK_21_HOME: ${{ env.JDK_21_HOME }} + JDK_25_HOME: ${{ env.JDK_25_HOME }} - name: Publish ByteCodeTranslator quality previews if: ${{ always() && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} diff --git a/vm/pom.xml b/vm/pom.xml index c1c2e0b132..973451b3b7 100644 --- a/vm/pom.xml +++ b/vm/pom.xml @@ -19,7 +19,7 @@ 1.8 1.8 UTF-8 - 9.2 + 9.8 5.10.2 diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java index 7ffd4c2abc..4a55df8d05 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java @@ -41,12 +41,20 @@ class BytecodeInstructionIntegrationTest { + static Stream provideCompilerConfigs() { + List configs = new ArrayList<>(); + configs.addAll(CompilerHelper.getAvailableCompilers("1.5")); + configs.addAll(CompilerHelper.getAvailableCompilers("1.8")); + configs.addAll(CompilerHelper.getAvailableCompilers("11")); + configs.addAll(CompilerHelper.getAvailableCompilers("17")); + configs.addAll(CompilerHelper.getAvailableCompilers("21")); + configs.addAll(CompilerHelper.getAvailableCompilers("25")); + return configs.stream(); + } + @ParameterizedTest - @ValueSource(strings = {"1.5", "1.8"}) - void translatesOptimizedBytecodeToLLVMExecutable(String targetVersion) throws Exception { - if ("1.5".equals(targetVersion) && !isSourceVersionSupported("1.5")) { - return; // Skip on newer JDKs that dropped 1.5 support - } + @org.junit.jupiter.params.provider.MethodSource("provideCompilerConfigs") + void translatesOptimizedBytecodeToLLVMExecutable(CompilerHelper.CompilerConfig config) throws Exception { Parser.cleanup(); Path sourceDir = Files.createTempDirectory("bytecode-integration-sources"); @@ -61,28 +69,43 @@ void translatesOptimizedBytecodeToLLVMExecutable(String targetVersion) throws Ex Path nativeReport = sourceDir.resolve("native_report.c"); Files.write(nativeReport, nativeReportSource().getBytes(StandardCharsets.UTF_8)); - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - assertNotNull(compiler, "A JDK is required to compile test sources"); - - // Compile App using JavaAPI as bootclasspath + // Compile App using the specific JDK List compileArgs = new ArrayList<>(); - compileArgs.add("-source"); - compileArgs.add(targetVersion); - compileArgs.add("-target"); - compileArgs.add(targetVersion); - compileArgs.add("-bootclasspath"); - compileArgs.add(javaApiDir.toString()); + + double targetVer = 1.8; + try { targetVer = Double.parseDouble(config.targetVersion); } catch (NumberFormatException ignored) {} + + double jdkVer = 1.8; + try { jdkVer = Double.parseDouble(config.jdkVersion); } catch (NumberFormatException ignored) {} + + if (jdkVer >= 9) { + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + // On JDK 9+, -bootclasspath is removed. + // --patch-module is not allowed with -target 8. + // We rely on the JDK's own bootstrap classes but include our JavaAPI in classpath + // so that any non-replaced classes are found. + // This means we compile against JDK 9+ API but emit older bytecode. + compileArgs.add("-classpath"); + compileArgs.add(javaApiDir.toString()); + } else { + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + compileArgs.add("-bootclasspath"); + compileArgs.add(javaApiDir.toString()); + compileArgs.add("-Xlint:-options"); + } + compileArgs.add("-d"); compileArgs.add(classesDir.toString()); compileArgs.add(sourceDir.resolve("BytecodeInstructionApp.java").toString()); - int compileResult = compiler.run( - null, - null, - null, - compileArgs.toArray(new String[0]) - ); - assertEquals(0, compileResult, "BytecodeInstructionApp should compile"); + int compileResult = CompilerHelper.compile(config.jdkHome, compileArgs); + assertEquals(0, compileResult, "BytecodeInstructionApp should compile with " + config); Files.copy(nativeReport, classesDir.resolve("native_report.c")); @@ -190,11 +213,8 @@ private String invokeLdcLocalVarsAppSource() { } @ParameterizedTest - @ValueSource(strings = {"1.5", "1.8"}) - void translatesInvokeAndLdcBytecodeToLLVMExecutable(String targetVersion) throws Exception { - if ("1.5".equals(targetVersion) && !isSourceVersionSupported("1.5")) { - return; // Skip on newer JDKs that dropped 1.5 support - } + @org.junit.jupiter.params.provider.MethodSource("provideCompilerConfigs") + void translatesInvokeAndLdcBytecodeToLLVMExecutable(CompilerHelper.CompilerConfig config) throws Exception { Parser.cleanup(); Path sourceDir = Files.createTempDirectory("invoke-ldc-sources"); @@ -209,27 +229,37 @@ void translatesInvokeAndLdcBytecodeToLLVMExecutable(String targetVersion) throws Path nativeReport = sourceDir.resolve("native_report.c"); Files.write(nativeReport, nativeReportSource().getBytes(StandardCharsets.UTF_8)); - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - assertNotNull(compiler, "A JDK is required to compile test sources"); - List compileArgs = new ArrayList<>(); - compileArgs.add("-source"); - compileArgs.add(targetVersion); - compileArgs.add("-target"); - compileArgs.add(targetVersion); - compileArgs.add("-bootclasspath"); - compileArgs.add(javaApiDir.toString()); + + double targetVer = 1.8; + try { targetVer = Double.parseDouble(config.targetVersion); } catch (NumberFormatException ignored) {} + + double jdkVer = 1.8; + try { jdkVer = Double.parseDouble(config.jdkVersion); } catch (NumberFormatException ignored) {} + + if (jdkVer >= 9) { + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + compileArgs.add("-classpath"); + compileArgs.add(javaApiDir.toString()); + } else { + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + compileArgs.add("-bootclasspath"); + compileArgs.add(javaApiDir.toString()); + compileArgs.add("-Xlint:-options"); + } + compileArgs.add("-d"); compileArgs.add(classesDir.toString()); compileArgs.add(sourceDir.resolve("InvokeLdcLocalVarsApp.java").toString()); - int compileResult = compiler.run( - null, - null, - null, - compileArgs.toArray(new String[0]) - ); - assertEquals(0, compileResult, "InvokeLdcLocalVarsApp should compile"); + int compileResult = CompilerHelper.compile(config.jdkHome, compileArgs); + assertEquals(0, compileResult, "InvokeLdcLocalVarsApp should compile with " + config); Files.copy(nativeReport, classesDir.resolve("native_report.c")); @@ -985,18 +1015,15 @@ private String nativeReportSource() { } @ParameterizedTest - @ValueSource(strings = {"1.5", "1.8"}) - void handleDefaultOutputWritesOutput(String targetVersion) throws Exception { - if ("1.5".equals(targetVersion) && !isSourceVersionSupported("1.5")) { - return; // Skip on newer JDKs that dropped 1.5 support - } + @org.junit.jupiter.params.provider.MethodSource("provideCompilerConfigs") + void handleDefaultOutputWritesOutput(CompilerHelper.CompilerConfig config) throws Exception { Parser.cleanup(); resetByteCodeClass(); Path sourceDir = Files.createTempDirectory("default-output-source"); Path outputDir = Files.createTempDirectory("default-output-dest"); Files.write(sourceDir.resolve("resource.txt"), "data".getBytes(StandardCharsets.UTF_8)); - compileDummyMainClass(sourceDir, "com.example", "MyAppDefault", targetVersion); + compileDummyMainClass(sourceDir, "com.example", "MyAppDefault", config); String[] args = new String[] { "csharp", @@ -1076,18 +1103,15 @@ void copyDirRecursivelyCopies() throws Exception { } @ParameterizedTest - @ValueSource(strings = {"1.5", "1.8"}) - void handleIosOutputGeneratesProjectStructure(String targetVersion) throws Exception { - if ("1.5".equals(targetVersion) && !isSourceVersionSupported("1.5")) { - return; // Skip on newer JDKs that dropped 1.5 support - } + @org.junit.jupiter.params.provider.MethodSource("provideCompilerConfigs") + void handleIosOutputGeneratesProjectStructure(CompilerHelper.CompilerConfig config) throws Exception { Parser.cleanup(); resetByteCodeClass(); Path sourceDir = Files.createTempDirectory("ios-output-source"); Path outputDir = Files.createTempDirectory("ios-output-dest"); Files.write(sourceDir.resolve("resource.txt"), "data".getBytes(StandardCharsets.UTF_8)); - compileDummyMainClass(sourceDir, "com.example", "MyAppIOS", targetVersion); + compileDummyMainClass(sourceDir, "com.example", "MyAppIOS", config); // Add a bundle to test copyDir invocation in execute loop Path bundleDir = sourceDir.resolve("test.bundle"); @@ -1139,7 +1163,7 @@ private void resetByteCodeClass() throws Exception { ((Set) writableFieldsField.get(null)).clear(); } - private void compileDummyMainClass(Path sourceDir, String packageName, String className, String targetVersion) throws Exception { + private void compileDummyMainClass(Path sourceDir, String packageName, String className, CompilerHelper.CompilerConfig config) throws Exception { Path packageDir = sourceDir; for (String part : packageName.split("\\.")) { packageDir = packageDir.resolve(part); @@ -1152,13 +1176,17 @@ private void compileDummyMainClass(Path sourceDir, String packageName, String cl "}\n"; Files.write(javaFile, content.getBytes(StandardCharsets.UTF_8)); - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - int result = compiler.run(null, null, null, - "-source", targetVersion, - "-target", targetVersion, - "-d", sourceDir.toString(), - javaFile.toString()); - assertEquals(0, result, "Compilation failed"); + List args = new ArrayList<>(); + args.add("-source"); + args.add(config.targetVersion); + args.add("-target"); + args.add(config.targetVersion); + args.add("-d"); + args.add(sourceDir.toString()); + args.add(javaFile.toString()); + + int result = CompilerHelper.compile(config.jdkHome, args); + assertEquals(0, result, "Compilation failed with " + config); } @Test @@ -1262,19 +1290,4 @@ void testArithmeticExpressionCoverage() { // or mock if possible. But here we can check basic behavior. } - private boolean isSourceVersionSupported(String version) { - String javaVersion = System.getProperty("java.specification.version"); - if (javaVersion.startsWith("1.")) { - return true; - } - try { - int major = Integer.parseInt(javaVersion); - if ("1.5".equals(version)) { - if (major >= 9) return false; - } - } catch (NumberFormatException e) { - return true; - } - return true; - } } diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index 026e60dd03..78f148ea92 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -26,11 +26,8 @@ class CleanTargetIntegrationTest { @ParameterizedTest - @ValueSource(strings = {"1.5", "1.8"}) - void generatesRunnableHelloWorldUsingCleanTarget(String targetVersion) throws Exception { - if ("1.5".equals(targetVersion) && !isSourceVersionSupported("1.5")) { - return; // Skip on newer JDKs that dropped 1.5 support - } + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void generatesRunnableHelloWorldUsingCleanTarget(CompilerHelper.CompilerConfig config) throws Exception { Parser.cleanup(); Path sourceDir = Files.createTempDirectory("clean-target-sources"); @@ -47,25 +44,47 @@ void generatesRunnableHelloWorldUsingCleanTarget(String targetVersion) throws Ex Files.write(sourceDir.resolve("java/lang/NullPointerException.java"), javaLangNullPointerExceptionSource().getBytes(StandardCharsets.UTF_8)); Files.write(sourceDir.resolve("native_hello.c"), nativeHelloSource().getBytes(StandardCharsets.UTF_8)); - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - assertNotNull(compiler, "A JDK is required to compile test sources"); - int compileResult = compiler.run( - null, - null, - null, - "-source", targetVersion, - "-target", targetVersion, - "-d", classesDir.toString(), - javaFile.toString(), - sourceDir.resolve("java/lang/Object.java").toString(), - sourceDir.resolve("java/lang/String.java").toString(), - sourceDir.resolve("java/lang/Class.java").toString(), - sourceDir.resolve("java/lang/Throwable.java").toString(), - sourceDir.resolve("java/lang/Exception.java").toString(), - sourceDir.resolve("java/lang/RuntimeException.java").toString(), - sourceDir.resolve("java/lang/NullPointerException.java").toString() - ); - assertEquals(0, compileResult, "HelloWorld.java should compile"); + List compileArgs = new java.util.ArrayList<>(); + + double jdkVer = 1.8; + try { jdkVer = Double.parseDouble(config.jdkVersion); } catch (NumberFormatException ignored) {} + + if (jdkVer >= 9) { + // For CleanTarget, we are compiling java.lang classes. + // On JDK 9+, this requires patching java.base. + // However, --patch-module is incompatible with -target 8. + // If we can't use --patch-module with -target 8, we skip this permutation. + if (Double.parseDouble(config.targetVersion) < 9) { + return; // Skip JDK 9+ compiling for Target < 9 for this specific test + } + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + compileArgs.add("--patch-module"); + compileArgs.add("java.base=" + sourceDir.toString()); + compileArgs.add("-Xlint:-module"); + } else { + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + compileArgs.add("-Xlint:-options"); + } + + compileArgs.add("-d"); + compileArgs.add(classesDir.toString()); + compileArgs.add(javaFile.toString()); + compileArgs.add(sourceDir.resolve("java/lang/Object.java").toString()); + compileArgs.add(sourceDir.resolve("java/lang/String.java").toString()); + compileArgs.add(sourceDir.resolve("java/lang/Class.java").toString()); + compileArgs.add(sourceDir.resolve("java/lang/Throwable.java").toString()); + compileArgs.add(sourceDir.resolve("java/lang/Exception.java").toString()); + compileArgs.add(sourceDir.resolve("java/lang/RuntimeException.java").toString()); + compileArgs.add(sourceDir.resolve("java/lang/NullPointerException.java").toString()); + + int compileResult = CompilerHelper.compile(config.jdkHome, compileArgs); + assertEquals(0, compileResult, "HelloWorld.java should compile with " + config); Files.copy(sourceDir.resolve("native_hello.c"), classesDir.resolve("native_hello.c")); @@ -461,19 +480,4 @@ static String nativeHelloSource() { "}\n"; } - private boolean isSourceVersionSupported(String version) { - String javaVersion = System.getProperty("java.specification.version"); - if (javaVersion.startsWith("1.")) { - return true; - } - try { - int major = Integer.parseInt(javaVersion); - if ("1.5".equals(version)) { - if (major >= 9) return false; - } - } catch (NumberFormatException e) { - return true; - } - return true; - } } diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java b/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java new file mode 100644 index 0000000000..a4ff6971b3 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java @@ -0,0 +1,138 @@ +package com.codename1.tools.translator; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Helper class to manage external JDK compilers. + */ +public class CompilerHelper { + + private static final Map availableJdks = new TreeMap<>(); + + static { + // Detect JDKs from environment variables set by CI or local setup + checkAndAddJdk("8", System.getenv("JDK_8_HOME")); + checkAndAddJdk("11", System.getenv("JDK_11_HOME")); + checkAndAddJdk("17", System.getenv("JDK_17_HOME")); + checkAndAddJdk("21", System.getenv("JDK_21_HOME")); + checkAndAddJdk("25", System.getenv("JDK_25_HOME")); + + // Fallback: If no env vars, assume current JVM is JDK 8 (or whatever is running) + // This ensures tests pass locally or in environments not fully configured with all JDKs + if (availableJdks.isEmpty()) { + String currentJavaHome = System.getProperty("java.home"); + // If it's a JRE, try to find JDK + if (currentJavaHome.endsWith("jre")) { + currentJavaHome = currentJavaHome.substring(0, currentJavaHome.length() - 4); + } + availableJdks.put(System.getProperty("java.specification.version"), Paths.get(currentJavaHome)); + } + } + + private static void checkAndAddJdk(String version, String path) { + if (path != null && !path.isEmpty() && new File(path).exists()) { + availableJdks.put(version, Paths.get(path)); + } + } + + public static List getAvailableCompilers(String targetVersion) { + List compilers = new ArrayList<>(); + + for (Map.Entry entry : availableJdks.entrySet()) { + String jdkVersion = entry.getKey(); + Path jdkHome = entry.getValue(); + + if (canCompile(jdkVersion, targetVersion)) { + compilers.add(new CompilerConfig(jdkVersion, jdkHome, targetVersion)); + } + } + + // If we are running in a constrained environment (e.g. local dev without env vars), + // we might not have found the specific JDK requested. + // If the list is empty, and target is 1.5 or 1.8, and we have *some* JDK, try to use it + // if it supports the target. + if (compilers.isEmpty() && !availableJdks.isEmpty()) { + Map.Entry defaultJdk = availableJdks.entrySet().iterator().next(); + if (canCompile(defaultJdk.getKey(), targetVersion)) { + compilers.add(new CompilerConfig(defaultJdk.getKey(), defaultJdk.getValue(), targetVersion)); + } + } + + return compilers; + } + + private static boolean canCompile(String compilerVersion, String targetVersion) { + try { + double compilerVer = Double.parseDouble(compilerVersion); + double targetVer = Double.parseDouble(targetVersion); + + // Java 9+ (version 9, 11, etc) dropped support for 1.5 + if (targetVer == 1.5) { + return compilerVer < 9; + } + // Java 21? dropped support for 1.6/1.7? + // Generally newer JDKs support 1.8+ + return compilerVer >= targetVer || (compilerVer >= 1.8 && targetVer <= 1.8); + } catch (NumberFormatException e) { + // Handle "1.8" format + if (compilerVersion.startsWith("1.")) { + return true; // Old JDKs support old targets + } + // Fallback for "25-ea" + if (compilerVersion.contains("-")) { + // Assume it's a new JDK + return !"1.5".equals(targetVersion); + } + return true; + } + } + + public static int compile(Path jdkHome, List args) throws IOException, InterruptedException { + String javac = jdkHome.resolve("bin").resolve("javac").toString(); + // On Windows it might be javac.exe + if (System.getProperty("os.name").toLowerCase().contains("win")) { + javac += ".exe"; + } + + List command = new ArrayList<>(); + command.add(javac); + + // Filter out flags that might be unsupported on newer JDKs if target is old, + // but generally we rely on the caller to provide correct flags. + // However, we might need to suppress warnings for obsolete targets. + // args.add("-Xlint:-options"); // Added by caller? + + command.addAll(args); + + ProcessBuilder pb = new ProcessBuilder(command); + // Inherit IO so we see errors in the log + pb.inheritIO(); + Process p = pb.start(); + return p.waitFor(); + } + + public static class CompilerConfig { + public final String jdkVersion; + public final Path jdkHome; + public final String targetVersion; + + public CompilerConfig(String jdkVersion, Path jdkHome, String targetVersion) { + this.jdkVersion = jdkVersion; + this.jdkHome = jdkHome; + this.targetVersion = targetVersion; + } + + @Override + public String toString() { + return "JDK " + jdkVersion + " (Target " + targetVersion + ")"; + } + } +}