Skip to content

Commit 22ae164

Browse files
Tudor-Stefan Magirescuolpaw
authored andcommitted
[GR-65623] Extend java.lang.module.ModuleReader#list with the ability to follow symlinks for directory modulepath entries during digest computation.
1 parent 96e0020 commit 22ae164

File tree

7 files changed

+224
-181
lines changed

7 files changed

+224
-181
lines changed

compiler/ci/ci_common/benchmark-suites.libsonnet

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,15 @@
9191

9292
barista_template(suite_version=null, suite_name="barista", max_jdk_version=null, cmd_app_prefix=["hwloc-bind --cpubind node:0.core:0-3.pu:0 --membind node:0"], non_prefix_barista_args=[]):: cc.compiler_benchmark + {
9393
suite:: suite_name,
94-
local barista_version = "v0.4.7",
94+
local barista_version = "v0.4.8",
9595
local suite_version_args = if suite_version != null then ["--bench-suite-version=" + suite_version] else [],
9696
local prefix_barista_arg = if std.length(cmd_app_prefix) > 0 then [std.format("--cmd-app-prefix=%s", std.join(" ", cmd_app_prefix))] else [],
9797
local all_barista_args = prefix_barista_arg + non_prefix_barista_args,
9898
local barista_args_with_separator = if std.length(all_barista_args) > 0 then ["--"] + all_barista_args else [],
9999
downloads+: {
100100
"WRK": { "name": "wrk", "version": "a211dd5", platformspecific: true},
101101
"WRK2": { "name": "wrk2", "version": "2.1", platformspecific: true},
102-
"BARISTA_BENCHMARKS": { "name": "barista", "version": "0.4.7"}
102+
"BARISTA_BENCHMARKS": { "name": "barista", "version": "0.4.8"}
103103
},
104104
packages+: {
105105
maven: "==3.8.6",

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/imagelayer/NativeImageLayers.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ Args = --layer-create=@layer-create.args
115115
The `layer-create.args` file-path is relative to the directory that contains the `native-image.properties` file and might look like this:
116116
```
117117
base-layer.nil
118+
# ignore this classpath/modulepath entry during layer compatibility checks
119+
digest-ignore
118120
module=java.base
119121
# micronaut and dependencies
120122
package=io.micronaut.*
@@ -129,6 +131,13 @@ package=org.reactivestreams.*
129131
Each line corresponds to one entry in the list of comma-separated entries that can usually be found in a regular `--layer-create` argument.
130132
Lines starting with `#` are ignored and can therefore be used to provide comments in such an option argument file.
131133

134+
Note the use of the `digest-ignore` suboption in the `layer-create.args` example file above.
135+
This suboption _only takes effect_ when the `--layer-create` option is specified _within a file_, and is ignored if provided via the command line.
136+
When defining `--layer-create` in a file, always include this suboption (see [compatibility rules](#compatibility-rules)).
137+
138+
Avoid placing other class or resource files in the same classpath/modulepath entry as the file(s) defining `--layer-create`.
139+
These files would be excluded from compatibility checks, potentially leading to subtle, difficult-to-debug issues.
140+
132141
### Option `--layer-use` consumes a shared layer, and can extend it or create a final executable:
133142

134143
```
@@ -195,16 +204,31 @@ native-image --module-path target/AwesomeLib-1.0-SNAPSHOT.jar --shared
195204

196205
### Compatibility rules
197206

198-
Currently, layer build compatibility checking is very limited and is only performed for the image builder arguments.
207+
Layer build compatibility checks are performed to ensure the consistency of _image builder arguments_ and _classpath/modulepath entries_.
208+
These will be covered in the following subsections.
209+
210+
#### Image builder arguments compatibility
211+
199212
The list below gives a few examples of checks that are already implemented.
200213

201214
- 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.
202215
Note that additional module system options not found in the previous image build are allowed to be used in the current image build.
203216
- Builder options of the form `-H:NeverInline=<pattern>` follow the same logic as the module system options above.
204217
- 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.
205218
- Other options like `-H:EntryPointNamePrefix=...`, `-H:APIFunctionPrefix=...`, ... follow the same logic as the `-g` option.
219+
- Environment variables provided using `-E` to the previous image build must be passed to the current image build, with their values remaining unchanged. Additional environment variables may be supplied to the current build if needed.
220+
221+
#### Classpath & modulepath compatibility
222+
223+
The classpath/modulepath entries used in the previous image layer build must also be included in the current image layer build.
224+
These entries must retain the same content. If any of the shared entries are modified (.e.g, updated jar files), the previous image layer must be rebuilt with the updated versions.
225+
226+
For example, assume the previous layer build used the classpath: `/path/to/foo.jar:/path/to/bar.jar`.
227+
Then the current layer build must include both `/path/to/foo.jar` and `/path/to/bar.jar`, possibly alongside additional entries: `path/to/foo.jar:/path/to/bar.jar:/path/to/extra.jar`.
228+
Additionally, the contents of `foo.jar` and `bar.jar` should remain unchanged between the two builds.
206229

207-
The number of checks is subject to change and will be further improved in the future.
230+
**Exception**: Classpath or modulepath entries that include a `native-image.properties` file specifying the `--layer-create` option with the `digest-ignore` suboption are exempt from this rule.
231+
These entries should be _excluded_ from subsequent layer builds.
208232

209233
### Limitations
210234

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageClassLoaderSupport.java

Lines changed: 125 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@
7474
import java.util.concurrent.TimeUnit;
7575
import java.util.concurrent.atomic.LongAdder;
7676
import java.util.function.BiConsumer;
77+
import java.util.function.Predicate;
7778
import java.util.jar.JarEntry;
7879
import java.util.jar.JarFile;
7980
import java.util.stream.Collector;
8081
import java.util.stream.Collectors;
8182
import java.util.stream.Stream;
8283
import java.util.zip.ZipFile;
8384

85+
import jdk.internal.module.Resources;
8486
import org.graalvm.collections.EconomicMap;
8587
import org.graalvm.collections.EconomicSet;
8688
import org.graalvm.collections.MapCursor;
@@ -126,7 +128,9 @@ public final class NativeImageClassLoaderSupport {
126128
private final List<Path> buildmp;
127129

128130
private final Set<Path> imageProvidedJars;
131+
/** Cleared by {@link #computePathEntryDigests()} on first call. */
129132
private PathDigests pathDigests;
133+
private final Class<?> explodedModuleReaderClass;
130134

131135
private final EconomicMap<URI, EconomicSet<String>> classes;
132136
private final EconomicMap<URI, EconomicSet<String>> packages;
@@ -289,6 +293,8 @@ protected NativeImageClassLoaderSupport(ClassLoader defaultSystemClassLoader, St
289293
annotationExtractor = new SubstrateAnnotationExtractor();
290294

291295
includeConfigSealed = false;
296+
297+
explodedModuleReaderClass = ReflectionUtil.lookupClass(false, "jdk.internal.module.ModuleReferences$ExplodedModuleReader");
292298
}
293299

294300
private static Stream<Path> toRealPath(Path p) {
@@ -311,11 +317,14 @@ public NativeImageClassLoader getClassLoader() {
311317
return classLoader;
312318
}
313319

314-
public Optional<PathDigests> getPathDigests(boolean free) {
315-
Optional<PathDigests> res = Optional.ofNullable(pathDigests);
316-
if (free) {
317-
pathDigests = null;
318-
}
320+
/**
321+
* The temporary {@link PathDigestEntry} object is used to create the list of
322+
* {@link PathDigestEntry} tuples. Note that the {@code pathDigests} field is cleared after the
323+
* first time this method is called.
324+
*/
325+
public List<PathDigestEntry> computePathEntryDigests() {
326+
List<PathDigestEntry> res = PathDigestEntry.aggregatePathDigests(pathDigests);
327+
pathDigests = null;
319328
return res;
320329
}
321330

@@ -986,23 +995,49 @@ private void initModule(ModuleReference moduleReference, boolean moduleRequiresI
986995
}
987996
final boolean isInImageModulePathOfLayeredBuild = pathDigests != null && pathDigests.mpDigests.containsKey(container);
988997
final boolean isJar = ClasspathUtils.isJar(Path.of(container));
989-
moduleReader.list().forEach(moduleResource -> {
990-
char fileSystemSeparatorChar = '/';
991-
String className = extractClassName(moduleResource, fileSystemSeparatorChar);
992-
if (className != null) {
993-
currentlyProcessedEntry = moduleReferenceLocation + fileSystemSeparatorChar + moduleResource;
994-
executor.execute(() -> handleClassFileName(container, module, className, includeUnconditionally, moduleRequiresInit, preserveModule));
995-
}
996-
if (isInImageModulePathOfLayeredBuild) {
997-
executor.execute(() -> PathDigests.storePathFileDigest(container, moduleResource, isJar, pathDigests.mpDigests));
998-
}
999-
entriesProcessed.increment();
1000-
});
998+
try (Stream<String> moduleResources = moduleReaderListFollowSymlinks(moduleReader, container)) {
999+
moduleResources.forEach(moduleResource -> {
1000+
char fileSystemSeparatorChar = '/';
1001+
String className = extractClassName(moduleResource, fileSystemSeparatorChar);
1002+
if (className != null) {
1003+
currentlyProcessedEntry = moduleReferenceLocation + fileSystemSeparatorChar + moduleResource;
1004+
executor.execute(() -> handleClassFileName(container, module, className, includeUnconditionally, moduleRequiresInit, preserveModule));
1005+
}
1006+
if (isInImageModulePathOfLayeredBuild) {
1007+
executor.execute(() -> PathDigests.storePathFileDigest(container, moduleResource, isJar, pathDigests.mpDigests));
1008+
}
1009+
entriesProcessed.increment();
1010+
});
1011+
}
10011012
} catch (IOException e) {
10021013
throw new RuntimeException("Unable get list of resources in module" + moduleReference.descriptor().name(), e);
10031014
}
10041015
}
10051016

1017+
/**
1018+
* This method extends {@link ModuleReader#list} with the ability to follow symlinks when
1019+
* the given {@link ModuleReader} corresponds to a
1020+
* {@code jdk.internal.module.ModuleReferences$ExplodedModuleReader} (i.e.,
1021+
* {@code container} is a directory which contains the contents of the module).
1022+
*
1023+
* Classloaders (e.g., {@link NativeImageClassLoader#findResource(String, String)}) can load
1024+
* resources pointed to by a symlink present on the modulepath, meaning the extension below
1025+
* is necessary if we want to keep track of such resources (e.g., for digest computation in
1026+
* layered builds).
1027+
*/
1028+
@SuppressWarnings("resource")
1029+
private Stream<String> moduleReaderListFollowSymlinks(ModuleReader reader, URI container) throws IOException {
1030+
if (explodedModuleReaderClass == null || !reader.getClass().isAssignableFrom(explodedModuleReaderClass)) {
1031+
return reader.list();
1032+
}
1033+
1034+
Path dir = Path.of(container);
1035+
// See {@code jdk.internal.module.ModuleReferences$ExplodedModuleReader#list}.
1036+
return Files.walk(dir, Integer.MAX_VALUE, FileVisitOption.FOLLOW_LINKS)
1037+
.map(f -> Resources.toResourceName(dir, f))
1038+
.filter(Predicate.not(String::isEmpty));
1039+
}
1040+
10061041
private void loadClassesFromPath(Path path) {
10071042
final boolean includeUnconditionally = layerSelectors.classpathEntries().contains(path);
10081043
final boolean includeAllMetadata = preserveSelectors.classpathEntries().contains(path);
@@ -1434,7 +1469,16 @@ public Set<IncludeOptionsSupport.PackageOptionValue> packages() {
14341469
}
14351470
}
14361471

1437-
public static final class PathDigests {
1472+
/**
1473+
* Stores a temporary collection of individual class/resource file digests that is updated
1474+
* during class loading. In particular, {@code PathDigests} objects store and update two
1475+
* {@link EconomicMap}s, one for classpath entries and the other for modulepath entries. Each
1476+
* {@link EconomicMap} maps a path entry (i.e., directory/jar) with the list of individual
1477+
* digests for the files it contains. The order of the individual digests is non-deterministic.
1478+
* After class loading, the {@code PathDigests} should be aggregated into a list of
1479+
* {@link PathDigestEntry}.
1480+
*/
1481+
private static final class PathDigests {
14381482
private final EconomicMap<URI, List<String>> cpDigests = EconomicMap.create();
14391483
private final EconomicMap<URI, List<String>> mpDigests = EconomicMap.create();
14401484

@@ -1475,12 +1519,73 @@ private static void storePathFileDigest(URI container, String resource, boolean
14751519
}
14761520
}
14771521

1478-
public EconomicMap<URI, List<String>> getCpDigests() {
1522+
private EconomicMap<URI, List<String>> getCpDigests() {
14791523
return cpDigests;
14801524
}
14811525

1482-
public EconomicMap<URI, List<String>> getMpDigests() {
1526+
private EconomicMap<URI, List<String>> getMpDigests() {
14831527
return mpDigests;
14841528
}
14851529
}
1530+
1531+
/**
1532+
* The record type {@link PathDigestEntry} encodes tuples of the form (type, digest, path),
1533+
* where: - type: is either {@code PathType.cp} or {@code PathType.mp} - digest: is a checksum
1534+
* which encodes information about every class/resource file part of that path (or reachable
1535+
* from a symlink part of that path) - path: is the absolute path ponting to the directory/jar
1536+
* included on the class/module-path. To obtain a list of all {@code PathDigestEntry} objects
1537+
* corresponding to a particular Native Image build, use the method
1538+
* {@link NativeImageClassLoaderSupport#computePathEntryDigests()}.
1539+
*/
1540+
public record PathDigestEntry(PathType type, String digest, String path) {
1541+
1542+
/**
1543+
* Aggregate the contents of {@link PathDigests} to create a list of
1544+
* {@link PathDigestEntry}. The individual file digests of a path entry are aggregated into
1545+
* a final digest that encodes information about all the contents of that path entry.
1546+
*/
1547+
private static List<PathDigestEntry> aggregatePathDigests(PathDigests pathDigests) {
1548+
Objects.requireNonNull(pathDigests, "NativeImageClassLoaderSupport#pathDigests should not be empty for a layered build.");
1549+
1550+
List<PathDigestEntry> aggregatedDigests = new ArrayList<>();
1551+
aggregatedDigests.addAll(aggregatePathDigests(pathDigests.getCpDigests(), PathType.cp));
1552+
aggregatedDigests.addAll(aggregatePathDigests(pathDigests.getMpDigests(), PathType.mp));
1553+
return aggregatedDigests;
1554+
}
1555+
1556+
private static List<PathDigestEntry> aggregatePathDigests(EconomicMap<URI, List<String>> pathDigests, PathType type) {
1557+
List<PathDigestEntry> aggregatedDigests = new ArrayList<>();
1558+
var cursor = pathDigests.getEntries();
1559+
while (cursor.advance()) {
1560+
aggregatedDigests.add(PathDigestEntry.of(type, cursor.getKey().getPath(), cursor.getValue()));
1561+
}
1562+
return aggregatedDigests;
1563+
}
1564+
1565+
private static PathDigestEntry of(PathType type, String path, List<String> digests) {
1566+
DigestBuilder db = new DigestBuilder();
1567+
digests.stream()
1568+
.sorted()
1569+
.map(d -> d.getBytes(StandardCharsets.UTF_8))
1570+
.forEach(db::update);
1571+
1572+
String aggregatedDigest = new String(db.digest(), StandardCharsets.UTF_8);
1573+
return new PathDigestEntry(type, aggregatedDigest, path);
1574+
}
1575+
1576+
public static PathDigestEntry of(String digestEntry) {
1577+
String[] envVarArr = digestEntry.split(":", 3);
1578+
return new PathDigestEntry(PathType.valueOf(envVarArr[0]), envVarArr[1], envVarArr[2]);
1579+
}
1580+
1581+
@Override
1582+
public String toString() {
1583+
return type + ":" + digest + ":" + path;
1584+
}
1585+
1586+
public enum PathType {
1587+
cp,
1588+
mp
1589+
}
1590+
}
14861591
}

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/imagelayer/HostedImageLayerBuildingSupport.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ private static String layerCreatePossibleOptions() {
100100
* {@link #initialize}.
101101
*/
102102
private final Function<Class<?>, SingletonTrait[]> singletonTraitInjector;
103+
/**
104+
* Optional suboption of the {@link SubstrateOptions#LayerCreate} option. If the `LayerCreate`
105+
* option is specified inside a `native-image.properties` file and this suboption is enabled,
106+
* the classpath/modulepath entry containing the `native-image.properties` file will be excluded
107+
* from the classpath/modulepath layered compatibility check. This suboption has no effect if
108+
* it's specified from the command line. See
109+
* {@link #processLayerOptions(EconomicMap, NativeImageClassLoaderSupport)} for more details.
110+
*/
111+
private static final String DIGEST_IGNORE = "digest-ignore";
103112

104113
private HostedImageLayerBuildingSupport(ImageClassLoader imageClassLoader,
105114
Reader snapshot, List<FileChannel> graphsChannels,
@@ -206,7 +215,7 @@ public static void processLayerOptions(EconomicMap<OptionKey<?>, Object> values,
206215
classLoaderSupport.setLayerFile(layerFile);
207216

208217
NativeImageClassLoaderSupport.IncludeSelectors layerSelectors = classLoaderSupport.getLayerSelectors();
209-
IncludeOptionsSupport.ExtendedOption digestIgnoreExtendedOption = new IncludeOptionsSupport.ExtendedOption("digest-ignore", null);
218+
IncludeOptionsSupport.ExtendedOption digestIgnoreExtendedOption = new IncludeOptionsSupport.ExtendedOption(DIGEST_IGNORE, null);
210219
for (IncludeOptionsSupport.ExtendedOption option : layerOption.extendedOptions()) {
211220
if (option.equals(digestIgnoreExtendedOption)) {
212221
digestIgnorePath = valueWithOrigin.origin().location();
@@ -295,7 +304,7 @@ private static String getLayerCreateValue(ValueWithOrigin<String> valueWithOrigi
295304
return String.join(",", OptionUtils.resolveOptionValuesRedirection(SubstrateOptions.LayerCreate, valueWithOrigin));
296305
}
297306

298-
public static boolean isLayerUseOptionEnabled(OptionValues values) {
307+
private static boolean isLayerUseOptionEnabled(OptionValues values) {
299308
if (SubstrateOptions.LayerUse.hasBeenSet(values)) {
300309
return !getLayerUseValue(values).toString().isEmpty();
301310
}

0 commit comments

Comments
 (0)