Skip to content

[GR-55581] Check layered image build option compatibility. #11207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ enum ArtifactType {
BUILD_INFO("build_info"),
/* For all debugging-related artifacts. */
DEBUG_INFO("debug_info"),
LAYER_SNAPSHOT("layer_snapshot"),
LAYER_SNAPSHOT_GRAPHS("layer_snapshot_graphs"),

/* For C header files. */
C_HEADER("c_headers"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,37 @@
*/
package com.oracle.svm.core;

import jdk.graal.compiler.options.Option;

import com.oracle.svm.core.option.APIOption;
import com.oracle.svm.core.option.HostedOptionKey;
import com.oracle.svm.core.option.AccumulatingLocatableMultiOptionValue;
import com.oracle.svm.core.option.HostedOptionKey;
import com.oracle.svm.core.option.LayerVerifiedOption;
import com.oracle.svm.core.option.LayerVerifiedOption.Kind;
import com.oracle.svm.core.option.LayerVerifiedOption.Severity;

import jdk.graal.compiler.options.Option;

public class NativeImageClassLoaderOptions {
public static final String AddExportsAndOpensFormat = "<module>/<package>=<target-module>(,<target-module>)*";
public static final String AddReadsFormat = "<module>=<target-module>(,<target-module>)*";

@LayerVerifiedOption(kind = Kind.Removed, severity = Severity.Error, positional = false)//
@APIOption(name = "add-exports", extra = true, launcherOption = true, valueSeparator = {APIOption.WHITESPACE_SEPARATOR, '='})//
@Option(help = "Value " + AddExportsAndOpensFormat + " updates <module> to export <package> to <target-module>, regardless of module declaration." +
" <target-module> can be ALL-UNNAMED to export to all unnamed modules.")//
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> AddExports = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build());

@LayerVerifiedOption(kind = Kind.Removed, severity = Severity.Error, positional = false)//
@APIOption(name = "add-opens", extra = true, launcherOption = true, valueSeparator = {APIOption.WHITESPACE_SEPARATOR, '='})//
@Option(help = "Value " + AddExportsAndOpensFormat + " updates <module> to open <package> to <target-module>, regardless of module declaration.")//
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> AddOpens = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build());

@LayerVerifiedOption(kind = Kind.Removed, severity = Severity.Error, positional = false)//
@APIOption(name = "add-reads", extra = true, valueSeparator = {APIOption.WHITESPACE_SEPARATOR, '='})//
@Option(help = "Value " + AddReadsFormat + " updates <module> to read <target-module>, regardless of module declaration." +
" <target-module> can be ALL-UNNAMED to read all unnamed modules.")//
public static final HostedOptionKey<AccumulatingLocatableMultiOptionValue.Strings> AddReads = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build());

@LayerVerifiedOption(kind = Kind.Removed, severity = Severity.Error)//
@APIOption(name = "enable-native-access", launcherOption = true, valueSeparator = {APIOption.WHITESPACE_SEPARATOR, '='})//
@Option(help = "A comma-separated list of modules that are permitted to perform restricted native operations." +
" The module name can also be ALL-UNNAMED.")//
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,25 @@ native-image --module-path target/AwesomeLib-1.0-SNAPSHOT.jar --shared
3. The layer specified by `--layer-use` must be compatible with the standalone command line.
The compatibility rules refer to:
- class/jar file compatibility (`-cp`, `-p`, same JDK, same GraalVM, same libs, etc.)
- config compatibility: GC config, etc.
- config compatibility: image builder options (e.g. GC config), etc.
- Java system properties and Environment variables compatibility
- access compatibility: no additional unsafe and field accesses are allowed

In case of incompatibility an error message is printed and the build process is aborted.
More information about compatibility checking can be found [below](#compatibility-rules)

### Compatibility rules

Currently, layer build compatibility checking is very limited and is only performed for the image builder arguments.
The list below gives a few examples of checks that are already implemented.

- Module system options `--add-exports`, `--add-opens`, `--add-reads` that were passed in the previous image build also need to be passed in the current image build.
Note that additional module system options not found in the previous image build are allowed to be used in the current image build.
- Builder options of the form `-H:NeverInline=<pattern>` follow the same logic as the module system options above.
- If debug option `-g` was passed in the previous image build it must also be passed in the current image build at the same position.
- Other options like `-H:EntryPointNamePrefix=...`, `-H:APIFunctionPrefix=...`, ... follow the same logic as the `-g` option.

The number of checks is subject to change and will be further improved in the future.

### Limitations

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package com.oracle.svm.core.option;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Use this annotation to mark image builder {@link HostedOptionKey}s and {@link RuntimeOptionKey}s
* that require layered image build compatibility checking. For example, if a {@code HostedOption}
* set to value {@code A} in the previous layer is required to also be set to the same value in the
* current layer, the respective option can use this annotation to enforce this requirement.
* Annotation elements {@code Severity}, {@code Kind}, {@code Positional} and {@code Message} can be
* used to configure the nature of checking that is required for the annotated option.
*
* @see LayerVerifiedOption.List
*/
@Repeatable(LayerVerifiedOption.List.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface LayerVerifiedOption {

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@interface List {
LayerVerifiedOption[] value();
}

/**
* Setting this element is required. It controls how a violation should be reported. Setting it
* to {@code Error} will cause the image build to abort with an error message in case of a
* violation. {@code Warn} reports the same message, but only as a warning, still allowing the
* image build to continue.
*/
Severity severity();

enum Severity {
Error,
Warn
}

/**
* When a violation is detected, a generic message for reporting the violation is automatically
* created. This annotation element allows to provide a message more specific to the annotated
* option.
*/
String message() default "";

/**
* The violation checking has three different variants to choose from. This annotation element
* is used to specify which kind is requested. Setting it to {@code Removed} reports a violation
* if an option was given in the previous layer build, but is missing in the current layer
* build. {@code Added} reports a violation if an option is specified in the current layer build
* but was not also used when the previous layer got built. {@code Changed} is the strictest
* form and requires an option to always be exactly the same between dependent layers.
*/
Kind kind();

enum Kind {
Removed,
Changed,
Added
}

/**
* The verification usually takes the position of the option within the sequence of options into
* account. For some options we can be less strict and allow the checking to be independent of
* the position. Specifying {@code false} selects this less strict mode. For example, if a
* previous layer was built with {@code --add-exports=foo/bar=ALL-UNNAMED}, the exact position
* where the current layer build specifies the same option is irrelevant as long as it does
* specify it somewhere in its sequence of options.
*/
boolean positional() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.jar.Attributes;
Expand Down Expand Up @@ -96,15 +97,15 @@ public Manifest createManifest(String mainClass) {
return mf;
}

public void expandJarToDir(Path inputJarFilePath, Path outputDir, AtomicBoolean outputDirDeleted) {
expandJarToDir(Function.identity(), inputJarFilePath, outputDir, outputDirDeleted);
public void expandJarToDir(Path inputJarFilePath, Path outputDir) {
expandJarToDir(Function.identity(), inputJarFilePath, outputDir, () -> false);
}

public void expandJarToDir(Function<Path, Path> relativizeEntry, Path inputJarFilePath, Path outputDir, AtomicBoolean outputDirDeleted) {
public void expandJarToDir(Function<Path, Path> relativizeEntry, Path inputJarFilePath, Path outputDir, BooleanSupplier outputDirDeleted) {
try {
try (JarFile archive = new JarFile(inputJarFilePath.toFile())) {
Enumeration<JarEntry> jarEntries = archive.entries();
while (jarEntries.hasMoreElements() && !outputDirDeleted.get()) {
while (jarEntries.hasMoreElements() && !outputDirDeleted.getAsBoolean()) {
JarEntry jarEntry = jarEntries.nextElement();
Path originalEntry = outputDir.resolve(jarEntry.getName());
Path targetEntry = relativizeEntry.apply(originalEntry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ private BundleSupport(NativeImage nativeImage, String bundleFilenameArg) {
outputDir = rootDir.resolve("output");

Path bundleFilePath = bundlePath.resolve(bundleName + BUNDLE_FILE_EXTENSION);
nativeImage.archiveSupport().expandJarToDir(e -> relativizeBundleEntry(getOriginalOutputDirName(), e), bundleFilePath, rootDir, deleteBundleRoot);
nativeImage.archiveSupport().expandJarToDir(e -> relativizeBundleEntry(getOriginalOutputDirName(), e), bundleFilePath, rootDir, deleteBundleRoot::get);

if (deleteBundleRoot.get()) {
/* Abort image build request without error message and exit with 0 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -515,21 +515,23 @@ public void run(Map<Method, CEntryPointData> entryPoints,
setSystemPropertiesForImageLate(k);

var hostedOptionValues = new HostedOptionValues(optionProvider.getHostedValues());
HostedImageLayerBuildingSupport imageLayerSupport = HostedImageLayerBuildingSupport.initialize(hostedOptionValues, loader);
ImageSingletonsSupportImpl.HostedManagement.install(new ImageSingletonsSupportImpl.HostedManagement(imageLayerSupport.buildingImageLayer), imageLayerSupport);

ImageSingletons.add(LayeredImageSingletonSupport.class, (LayeredImageSingletonSupport) ImageSingletonsSupportImpl.get());
ImageSingletons.add(ProgressReporter.class, reporter);
ImageSingletons.add(DeadlockWatchdog.class, loader.watchdog);
ImageSingletons.add(TimerCollection.class, timerCollection);
ImageSingletons.add(ImageBuildStatistics.TimerCollectionPrinter.class, timerCollection);
ImageSingletons.add(AnnotationExtractor.class, loader.classLoaderSupport.annotationExtractor);
ImageSingletons.add(BuildArtifacts.class, new BuildArtifactsImpl());
ImageSingletons.add(HostedOptionValues.class, hostedOptionValues);
ImageSingletons.add(RuntimeOptionValues.class, new RuntimeOptionValues(optionProvider.getRuntimeValues(), allOptionNames));

try (TemporaryBuildDirectoryProviderImpl tempDirectoryProvider = new TemporaryBuildDirectoryProviderImpl()) {
var tempDirectoryOptionValue = NativeImageOptions.TempDirectory.getValue(hostedOptionValues).lastValue().orElse(null);
try (TemporaryBuildDirectoryProviderImpl tempDirectoryProvider = new TemporaryBuildDirectoryProviderImpl(tempDirectoryOptionValue)) {
var builderTempDir = tempDirectoryProvider.getTemporaryBuildDirectory();
HostedImageLayerBuildingSupport imageLayerSupport = HostedImageLayerBuildingSupport.initialize(hostedOptionValues, loader, builderTempDir);
ImageSingletonsSupportImpl.HostedManagement.install(new ImageSingletonsSupportImpl.HostedManagement(imageLayerSupport.buildingImageLayer), imageLayerSupport);

ImageSingletons.add(LayeredImageSingletonSupport.class, (LayeredImageSingletonSupport) ImageSingletonsSupportImpl.get());
ImageSingletons.add(ProgressReporter.class, reporter);
ImageSingletons.add(DeadlockWatchdog.class, loader.watchdog);
ImageSingletons.add(TimerCollection.class, timerCollection);
ImageSingletons.add(ImageBuildStatistics.TimerCollectionPrinter.class, timerCollection);
ImageSingletons.add(AnnotationExtractor.class, loader.classLoaderSupport.annotationExtractor);
ImageSingletons.add(BuildArtifacts.class, new BuildArtifactsImpl());
ImageSingletons.add(HostedOptionValues.class, hostedOptionValues);
ImageSingletons.add(RuntimeOptionValues.class, new RuntimeOptionValues(optionProvider.getRuntimeValues(), allOptionNames));
ImageSingletons.add(TemporaryBuildDirectoryProvider.class, tempDirectoryProvider);

doRun(entryPoints, javaMainSupport, imageName, k, harnessSubstitutions);
} finally {
reporter.ensureCreationStageEndCompleted();
Expand Down Expand Up @@ -563,7 +565,7 @@ protected void doRun(Map<Method, CEntryPointData> entryPoints, JavaMainSupport j

try (DebugContext debug = new Builder(options, new GraalDebugHandlersFactory(GraalAccess.getOriginalSnippetReflection())).build();
DebugCloseable featureCleanup = () -> featureHandler.forEachFeature(Feature::cleanup)) {
setupNativeImage(imageName, options, entryPoints, javaMainSupport, harnessSubstitutions, debug);
setupNativeImage(options, entryPoints, javaMainSupport, harnessSubstitutions, debug);

boolean returnAfterAnalysis = runPointsToAnalysis(imageName, options, debug);
if (returnAfterAnalysis) {
Expand Down Expand Up @@ -758,7 +760,7 @@ protected void doRun(Map<Method, CEntryPointData> entryPoints, JavaMainSupport j
try (StopTimer t = TimerCollection.createTimerAndStart(TimerCollection.Registry.ARCHIVE_LAYER)) {
if (ImageLayerBuildingSupport.buildingSharedLayer()) {
ImageSingletonsSupportImpl.HostedManagement.persist();
HostedImageLayerBuildingSupport.singleton().archiveLayer(imageName);
HostedImageLayerBuildingSupport.singleton().archiveLayer();
}
}
reporter.printCreationEnd(image.getImageFileSize(), heap.getLayerObjectCount(), image.getImageHeapSize(), codeCache.getCodeAreaSize(), numCompilations, image.getDebugInfoSize(),
Expand Down Expand Up @@ -917,7 +919,7 @@ protected boolean verifyAssignableTypes() {
}

@SuppressWarnings("try")
protected void setupNativeImage(String imageName, OptionValues options, Map<Method, CEntryPointData> entryPoints, JavaMainSupport javaMainSupport,
protected void setupNativeImage(OptionValues options, Map<Method, CEntryPointData> entryPoints, JavaMainSupport javaMainSupport,
SubstitutionProcessor harnessSubstitutions, DebugContext debug) {
try (Indent ignored = debug.logAndIndent("setup native-image builder")) {
try (StopTimer ignored1 = TimerCollection.createTimerAndStart(TimerCollection.Registry.SETUP)) {
Expand Down Expand Up @@ -996,7 +998,6 @@ protected void setupNativeImage(String imageName, OptionValues options, Map<Meth
if (ImageLayerBuildingSupport.buildingSharedLayer()) {
SVMImageLayerWriter imageLayerWriter = HostedConfiguration.instance().createSVMImageLayerWriter(imageLayerSnapshotUtil, useSharedLayerGraphs, useSharedLayerStrengthenedGraphs);
HostedImageLayerBuildingSupport.singleton().setWriter(imageLayerWriter);
HostedImageLayerBuildingSupport.setupImageLayerArtifacts(imageName);
}

if (ImageLayerBuildingSupport.buildingExtensionLayer()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,30 @@
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Optional;

import com.oracle.svm.core.c.libc.TemporaryBuildDirectoryProvider;
import com.oracle.svm.core.util.TimeUtils;
import com.oracle.svm.core.util.VMError;

public class TemporaryBuildDirectoryProviderImpl implements TemporaryBuildDirectoryProvider, AutoCloseable {

private final Path tempDirectoryOptionValue;
private Path tempDirectory;
private boolean deleteTempDirectory;

public TemporaryBuildDirectoryProviderImpl(Path tempDirectoryOptionValue) {
this.tempDirectoryOptionValue = tempDirectoryOptionValue;
}

@Override
public synchronized Path getTemporaryBuildDirectory() {
if (tempDirectory == null) {
try {
Optional<Path> tempName = NativeImageOptions.TempDirectory.getValue().lastValue();
if (tempName.isEmpty()) {
if (tempDirectoryOptionValue == null) {
tempDirectory = Files.createTempDirectory("SVM-");
deleteTempDirectory = true;
} else {
tempDirectory = tempName.get().resolve("SVM-" + TimeUtils.currentTimeMillis());
tempDirectory = tempDirectoryOptionValue.resolve("SVM-" + TimeUtils.currentTimeMillis());
assert !Files.exists(tempDirectory);
Files.createDirectories(tempDirectory);
}
Expand Down
Loading