Skip to content

Commit 790fa05

Browse files
authored
Merge pull request #822 from graalvm/vj/compatibility-mode
Support `-H:+CompatibilityMode` in build tools
2 parents 1844654 + 0631241 commit 790fa05

5 files changed

Lines changed: 220 additions & 35 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,8 @@ nb-configuration.xml
8585
## OS X
8686
##############################
8787
.DS_Store
88+
89+
##############################
90+
# Vibe Coding
91+
##############################
92+
scratchpad/

docs/src/docs/asciidoc/changelog.adoc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
[[changelog]]
22
== Changelog
33

4+
== Release 0.11.5
5+
6+
- Feature – Auto-fallback to JVM JUnit when -H:+CompatibilityMode is enabled
7+
+
8+
* When -H:+CompatibilityMode is present in native-image options, native-image JUnit tests are disabled and JVM tests run automatically.
9+
* Detection sources: native-image build arguments and NATIVE_IMAGE_OPTIONS environment variable.
10+
* Gradle: test image wiring for the native JUnit launcher and JUnit Platform feature is gated; native test image build/run tasks are skipped via onlyIf; once-per-build log:
11+
+
12+
"Compatibility Mode detected (-H:+CompatibilityMode); The native test image will be built using the original JUnit ConsoleLauncher."
13+
14+
* Maven: native test goal short-circuits after environment setup; once-per-build log:
15+
+
16+
"Compatibility Mode detected (-H:+CompatibilityMode); The native test image will be built using the original JUnit ConsoleLauncher."
17+
* Default behavior unchanged when the flag is absent (native-image JUnit path continues as before).
18+
419
== Release 0.11.3
520

621
- Fixed use of argument file when the temporary directory is not on the same drive as the project
@@ -449,7 +464,6 @@ graalvmNative {
449464
}
450465
}
451466
----
452-
453467
- The `nativeBuild` task has been renamed to `nativeCompile`.
454468
- The `nativeTestBuild` task has been renamed to `nativeTestCompile`.
455469

native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java

Lines changed: 121 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.graalvm.buildtools.gradle.internal.GraalVMLogger;
5656
import org.graalvm.buildtools.gradle.internal.GraalVMReachabilityMetadataService;
5757
import org.graalvm.buildtools.gradle.internal.GradleUtils;
58+
import org.graalvm.buildtools.gradle.internal.NativeImageExecutableLocator;
5859
import org.graalvm.buildtools.gradle.internal.agent.AgentConfigurationFactory;
5960
import org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask;
6061
import org.graalvm.buildtools.gradle.tasks.CollectReachabilityMetadata;
@@ -65,9 +66,9 @@
6566
import org.graalvm.buildtools.gradle.tasks.UseLayerOptions;
6667
import org.graalvm.buildtools.gradle.tasks.actions.CleanupAgentFilesAction;
6768
import org.graalvm.buildtools.gradle.tasks.actions.MergeAgentFilesAction;
69+
import org.graalvm.buildtools.gradle.tasks.scanner.JarAnalyzerTransform;
6870
import org.graalvm.buildtools.utils.JUnitPlatformNativeDependenciesHelper;
6971
import org.graalvm.buildtools.utils.JUnitUtils;
70-
import org.graalvm.buildtools.gradle.tasks.scanner.JarAnalyzerTransform;
7172
import org.graalvm.buildtools.utils.SharedConstants;
7273
import org.graalvm.reachability.DirectoryConfiguration;
7374
import org.gradle.api.Action;
@@ -100,7 +101,6 @@
100101
import org.gradle.api.plugins.JavaLibraryPlugin;
101102
import org.gradle.api.plugins.JavaPlugin;
102103
import org.gradle.api.plugins.JavaPluginExtension;
103-
import org.gradle.api.provider.ListProperty;
104104
import org.gradle.api.provider.MapProperty;
105105
import org.gradle.api.provider.Property;
106106
import org.gradle.api.provider.Provider;
@@ -176,8 +176,15 @@ public class NativeImagePlugin implements Plugin<Project> {
176176
private static final String REPOSITORY_COORDINATES = "org.graalvm.buildtools:graalvm-reachability-metadata:" + VersionInfo.NBT_VERSION + ":repository@zip";
177177
private static final String DEFAULT_URI = String.format(METADATA_REPO_URL_TEMPLATE, VersionInfo.METADATA_REPO_VERSION);
178178

179+
// Compatibility Mode detection constants
180+
private static final String COMPATIBILITY_MODE_TOKEN = "-H:+CompatibilityMode";
181+
private static final String NATIVE_IMAGE_OPTIONS_ENV = "NATIVE_IMAGE_OPTIONS";
182+
179183
private GraalVMLogger logger;
180184

185+
// Exposed detection provider for test binaries (to be used by follow-up tasks)
186+
private Provider<Boolean> compatModeEnabled;
187+
181188
@Inject
182189
public ArchiveOperations getArchiveOperations() {
183190
throw new UnsupportedOperationException();
@@ -707,6 +714,71 @@ public void registerTestBinary(Project project,
707714
// Add DSL extension for testing
708715
NativeImageOptions testOptions = createTestOptions(graalExtension, name, project, mainOptions, config.getSourceSet());
709716

717+
// Compute and expose the Compatibility Mode detection provider for test binary
718+
this.compatModeEnabled = computeCompatibilityModeEnabledProvider(project, testOptions);
719+
720+
// Unified once-per-build log at configuration time if Compatibility Mode is enabled
721+
project.afterEvaluate(p -> {
722+
if (compatModeEnabled().getOrElse(false)) {
723+
logger.logOnce("Compatibility Mode detected (-H:+CompatibilityMode); The native test image will be built using the original JUnit ConsoleLauncher.");
724+
}
725+
});
726+
727+
// Wire main class based on Compatibility Mode, mirroring Maven plugin behavior.
728+
testOptions.getMainClass().convention(compatModeEnabled().map(c -> c
729+
? "org.junit.platform.console.ConsoleLauncher"
730+
: "org.graalvm.junit.platform.NativeImageJUnitLauncher"));
731+
732+
// Add the JUnit Platform Feature flag and exclude JUnit class init files only when NOT in Compatibility Mode.
733+
final String junitPlatformFeatureFlag = "--features=org.graalvm.junit.platform.JUnitPlatformFeature";
734+
project.afterEvaluate(p -> {
735+
boolean compat = compatModeEnabled().getOrElse(false);
736+
if (!compat) {
737+
List<String> current = testOptions.getBuildArgs().getOrElse(Collections.emptyList());
738+
if (!current.contains(junitPlatformFeatureFlag)) {
739+
testOptions.getBuildArgs().add(junitPlatformFeatureFlag);
740+
}
741+
/* in version 5.12.0 JUnit added initialize-at-build-time properties files which we need to exclude */
742+
testOptions.getBuildArgs().addAll(JUnitUtils.excludeJUnitClassInitializationFiles());
743+
}
744+
});
745+
// Add XML output dir only in regular mode (not in Compatibility Mode)
746+
Provider<String> xmlOutputDir = project.getLayout().getBuildDirectory()
747+
.dir("test-results/" + name + "-native")
748+
.map(d -> d.getAsFile().getAbsolutePath());
749+
testOptions.getRuntimeArgs().addAll(
750+
compatModeEnabled().zip(xmlOutputDir, serializableBiFunctionOf((compat, dir) ->
751+
compat ? Collections.emptyList() : Arrays.asList("--xml-output-dir", dir)
752+
))
753+
);
754+
// In Compatibility Mode, pass classpath and scan directive to the JUnit ConsoleLauncher to avoid
755+
// "Please specify an explicit selector option or use --scan-class-path or --scan-modules"
756+
Provider<String> cpString = project.getProviders().provider(() -> testOptions.getClasspath().getAsPath());
757+
testOptions.getRuntimeArgs().addAll(
758+
compatModeEnabled().zip(cpString, serializableBiFunctionOf((compat, cp) ->
759+
compat ? Arrays.asList("-Djava.class.path=" + cp, "--scan-classpath") : Collections.<String>emptyList()
760+
))
761+
);
762+
// In Compatibility Mode, also pass -Djava.home from the GraalVM used for the build
763+
Provider<String> graalVmHome = project.getProviders().provider(() -> {
764+
NativeImageExecutableLocator.Diagnostics d = new NativeImageExecutableLocator.Diagnostics();
765+
File nativeImage = NativeImageExecutableLocator.findNativeImageExecutable(
766+
testOptions.getJavaLauncher(),
767+
graalExtension.getToolchainDetection().map(enabled -> !enabled),
768+
graalvmHomeProvider(project.getProviders(), d),
769+
getExecOperations(),
770+
logger,
771+
d
772+
);
773+
File parent = nativeImage.getParentFile();
774+
return parent != null ? parent.getParent() : null;
775+
});
776+
testOptions.getRuntimeArgs().addAll(
777+
compatModeEnabled().zip(graalVmHome, serializableBiFunctionOf((compat, home) ->
778+
compat && home != null ? Collections.singletonList("-Djava.home=" + home) : Collections.<String>emptyList()
779+
))
780+
);
781+
710782
TaskProvider<Test> testTask = config.validate().getTestTask();
711783
testTask.configure(test -> {
712784
var testList = testResultsDir.dir(test.getName() + "/testlist");
@@ -850,23 +922,13 @@ private NativeImageOptions createTestOptions(GraalVMExtension graalExtension,
850922
var configurations = project.getConfigurations();
851923
setupExtensionConfigExcludes(testExtension, configurations);
852924

853-
testExtension.getMainClass().set("org.graalvm.junit.platform.NativeImageJUnitLauncher");
854-
testExtension.getMainClass().finalizeValue();
855925
testExtension.getImageName().convention(mainExtension.getImageName().map(name -> name + SharedConstants.NATIVE_TESTS_SUFFIX));
856926

857-
ListProperty<String> runtimeArgs = testExtension.getRuntimeArgs();
858-
runtimeArgs.add("--xml-output-dir");
859-
runtimeArgs.add(project.getLayout().getBuildDirectory().dir("test-results/" + binaryName + "-native").map(d -> d.getAsFile().getAbsolutePath()));
860-
861-
testExtension.buildArgs("--features=org.graalvm.junit.platform.JUnitPlatformFeature");
862927
ConfigurableFileCollection classpath = testExtension.getClasspath();
863928
classpath.from(configurations.getByName(imageClasspathConfigurationNameFor(binaryName)));
864929
classpath.from(sourceSet.getOutput().getClassesDirs());
865930
classpath.from(sourceSet.getOutput().getResourcesDir());
866931

867-
/* in version 5.12.0 JUnit added initialize-at-build-time properties files which we need to exclude */
868-
testExtension.getBuildArgs().addAll(JUnitUtils.excludeJUnitClassInitializationFiles());
869-
870932
return testExtension;
871933
}
872934

@@ -1069,4 +1131,51 @@ public List<String> getExcludes() {
10691131
}
10701132
}
10711133

1134+
// -----------------------------
1135+
// Compatibility Mode Detection
1136+
// -----------------------------
1137+
1138+
/**
1139+
* Exposes the Compatibility Mode detection for the test binary.
1140+
* Follow-up tasks will use this provider to gate configuration/skip tasks.
1141+
*/
1142+
public Provider<Boolean> compatModeEnabled() {
1143+
return compatModeEnabled;
1144+
}
1145+
1146+
private static Provider<Boolean> computeCompatibilityModeEnabledProvider(Project project, NativeImageOptions options) {
1147+
ProviderFactory providers = project.getProviders();
1148+
1149+
// System environment: NATIVE_IMAGE_OPTIONS
1150+
Provider<Boolean> fromSystemEnv = providers.environmentVariable(NATIVE_IMAGE_OPTIONS_ENV)
1151+
.map(NativeImagePlugin::containsCompatibilityTokenInString)
1152+
.orElse(false);
1153+
1154+
// Options-level environment variables
1155+
Provider<Boolean> fromOptionsEnv = options.getEnvironmentVariables()
1156+
.map(env -> {
1157+
Object v = env.get(NATIVE_IMAGE_OPTIONS_ENV);
1158+
return v != null && containsCompatibilityTokenInString(String.valueOf(v));
1159+
})
1160+
.orElse(false);
1161+
1162+
// Build args on the test options
1163+
Provider<Boolean> fromBuildArgs = options.getBuildArgs()
1164+
.map(NativeImagePlugin::containsCompatibilityTokenInArgs)
1165+
.orElse(false);
1166+
1167+
// Combine: true if any source enables compatibility mode
1168+
Provider<Boolean> anyEnv = fromSystemEnv.zip(fromOptionsEnv, (a, b) -> a || b);
1169+
return anyEnv.zip(fromBuildArgs, (ab, c) -> ab || c);
1170+
}
1171+
1172+
private static boolean containsCompatibilityTokenInString(String value) {
1173+
return value != null && value.contains(COMPATIBILITY_MODE_TOKEN);
1174+
}
1175+
1176+
public static boolean containsCompatibilityTokenInArgs(List<String> args) {
1177+
return args != null && args.stream()
1178+
.filter(Objects::nonNull)
1179+
.anyMatch(s -> s.equals(COMPATIBILITY_MODE_TOKEN));
1180+
}
10721181
}

native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,8 +592,8 @@ protected void maybeAddGeneratedResourcesConfig(List<String> into) {
592592
}
593593

594594
protected void maybeAddDynamicAccessMetadataToClasspath() {
595-
if (Files.exists(Path.of(outputDirectory.getPath(),"dynamic-access-metadata.json"))) {
596-
imageClasspath.add(Path.of(outputDirectory.getPath(),"dynamic-access-metadata.json"));
595+
if (Files.exists(Path.of(outputDirectory.getPath(), "dynamic-access-metadata.json"))) {
596+
imageClasspath.add(Path.of(outputDirectory.getPath(), "dynamic-access-metadata.json"));
597597
}
598598
}
599599

0 commit comments

Comments
 (0)