Skip to content

Commit 54db68c

Browse files
authored
Generate dynamic access metadata and provide it to the classpath when passing "--emit build-report" as a build argument (#795)
1 parent bd18af3 commit 54db68c

File tree

8 files changed

+528
-1
lines changed

8 files changed

+528
-1
lines changed

common/graalvm-reachability-metadata/src/main/java/org/graalvm/reachability/internal/FileSystemRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ public Set<DirectoryConfiguration> findConfigurationsFor(Consumer<? super Query>
133133
.collect(Collectors.toSet());
134134
}
135135

136+
public Path getRootDirectory() {
137+
return rootDirectory;
138+
}
139+
136140
/**
137141
* Allows getting insights about how configuration is picked.
138142
*/
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.graalvm.buildtools.utils;
2+
3+
import com.github.openjson.JSONArray;
4+
import com.github.openjson.JSONObject;
5+
6+
import java.io.File;
7+
import java.io.FileWriter;
8+
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.util.LinkedHashSet;
11+
import java.util.Map;
12+
import java.util.Set;
13+
14+
public final class DynamicAccessMetadataUtils {
15+
/**
16+
* Collects all versionless artifact coordinates ({@code groupId:artifactId}) from each
17+
* entry in the {@code library-and-framework-list.json} file.
18+
*/
19+
public static Set<String> readArtifacts(File inputFile) throws IOException {
20+
Set<String> artifacts = new LinkedHashSet<>();
21+
String content = Files.readString(inputFile.toPath());
22+
JSONArray jsonArray = new JSONArray(content);
23+
for (int i = 0; i < jsonArray.length(); i++) {
24+
JSONObject entry = jsonArray.getJSONObject(i);
25+
if (entry.has("artifact")) {
26+
artifacts.add(entry.getString("artifact"));
27+
}
28+
}
29+
return artifacts;
30+
}
31+
32+
/**
33+
* Serializes dynamic access metadata to JSON.
34+
* <p>
35+
* The output follows the schema defined at:
36+
* <a href="https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/dynamic-access-metadata-schema-v1.0.0.json">
37+
* dynamic-access-metadata-schema-v1.0.0.json
38+
* </a>
39+
*/
40+
public static void serialize(File outputFile, Map<String, Set<String>> exportMap) throws IOException {
41+
JSONArray jsonArray = new JSONArray();
42+
43+
for (Map.Entry<String, Set<String>> entry : exportMap.entrySet()) {
44+
JSONObject obj = new JSONObject();
45+
obj.put("metadataProvider", entry.getKey());
46+
47+
JSONArray providedArray = new JSONArray();
48+
entry.getValue().forEach(providedArray::put);
49+
obj.put("providesFor", providedArray);
50+
51+
jsonArray.put(obj);
52+
}
53+
54+
try (FileWriter writer = new FileWriter(outputFile)) {
55+
writer.write(jsonArray.toString(2));
56+
}
57+
}
58+
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.graalvm.buildtools.gradle.internal.agent.AgentConfigurationFactory;
5959
import org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask;
6060
import org.graalvm.buildtools.gradle.tasks.CollectReachabilityMetadata;
61+
import org.graalvm.buildtools.gradle.tasks.GenerateDynamicAccessMetadata;
6162
import org.graalvm.buildtools.gradle.tasks.GenerateResourcesConfigFile;
6263
import org.graalvm.buildtools.gradle.tasks.MetadataCopyTask;
6364
import org.graalvm.buildtools.gradle.tasks.NativeRunTask;
@@ -92,6 +93,7 @@
9293
import org.gradle.api.file.FileCollection;
9394
import org.gradle.api.file.FileSystemLocation;
9495
import org.gradle.api.file.FileSystemOperations;
96+
import org.gradle.api.file.RegularFile;
9597
import org.gradle.api.logging.LogLevel;
9698
import org.gradle.api.plugins.ExtensionAware;
9799
import org.gradle.api.plugins.JavaApplication;
@@ -389,6 +391,27 @@ private void configureAutomaticTaskCreation(Project project,
389391
options.getConfigurationFileDirectories().from(generateResourcesConfig.map(serializableTransformerOf(t ->
390392
t.getOutputFile().map(serializableTransformerOf(f -> f.getAsFile().getParentFile()))
391393
)));
394+
TaskProvider<GenerateDynamicAccessMetadata> generateDynamicAccessMetadata = registerDynamicAccessMetadataTask(
395+
project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME),
396+
graalVMReachabilityMetadataService(project, reachabilityExtensionOn(graalExtension)),
397+
project.getLayout().getBuildDirectory(),
398+
tasks,
399+
deriveTaskName(binaryName, "generate", "DynamicAccessMetadata"));
400+
imageBuilder.configure(buildImageTask -> {
401+
Provider<Boolean> emittingBuildReport =
402+
buildImageTask.getOptions()
403+
.flatMap(o -> o.getBuildArgs()
404+
.map(args -> args.stream()
405+
.anyMatch(arg -> arg.startsWith("--emit build-report"))));
406+
options.getClasspath().from(
407+
emittingBuildReport.flatMap(enabled ->
408+
enabled
409+
? generateDynamicAccessMetadata.flatMap(task ->
410+
task.getOutputJson().map(RegularFile::getAsFile))
411+
: buildImageTask.getProject().provider(Collections::emptyList))
412+
);
413+
});
414+
392415
configureJvmReachabilityConfigurationDirectories(project, graalExtension, options, sourceSet);
393416
configureJvmReachabilityExcludeConfigArgs(project, graalExtension, options, sourceSet);
394417
});
@@ -656,6 +679,18 @@ private TaskProvider<GenerateResourcesConfigFile> registerResourcesConfigTask(Pr
656679
});
657680
}
658681

682+
private TaskProvider<GenerateDynamicAccessMetadata> registerDynamicAccessMetadataTask(Configuration classpathConfiguration,
683+
Provider<GraalVMReachabilityMetadataService> metadataService,
684+
DirectoryProperty buildDir,
685+
TaskContainer tasks,
686+
String name) {
687+
return tasks.register(name, GenerateDynamicAccessMetadata.class, task -> {
688+
task.setClasspath(classpathConfiguration);
689+
task.getMetadataService().set(metadataService);
690+
task.getOutputJson().set(buildDir.dir("generated").map(dir -> dir.file("dynamic-access-metadata.json")));
691+
});
692+
}
693+
659694
public void registerTestBinary(Project project,
660695
DefaultGraalVmExtension graalExtension,
661696
DefaultTestBinaryConfig config) {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import java.util.Collection;
7272
import java.util.Map;
7373
import java.util.Objects;
74+
import java.util.Optional;
7475
import java.util.Set;
7576
import java.util.function.Consumer;
7677
import java.util.function.Supplier;
@@ -235,4 +236,11 @@ public Set<DirectoryConfiguration> findConfigurationsFor(Set<String> excludedMod
235236
query.useLatestConfigWhenVersionIsUntested();
236237
});
237238
}
239+
240+
public Optional<Path> getRepositoryDirectory() {
241+
if (repository instanceof FileSystemRepository fsRepo) {
242+
return Optional.of(fsRepo.getRootDirectory());
243+
}
244+
return Optional.empty();
245+
}
238246
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package org.graalvm.buildtools.gradle.tasks;
42+
43+
import org.graalvm.buildtools.gradle.internal.GraalVMLogger;
44+
import org.graalvm.buildtools.gradle.internal.GraalVMReachabilityMetadataService;
45+
import org.graalvm.buildtools.utils.DynamicAccessMetadataUtils;
46+
import org.gradle.api.DefaultTask;
47+
import org.gradle.api.artifacts.Configuration;
48+
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
49+
import org.gradle.api.artifacts.result.DependencyResult;
50+
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
51+
import org.gradle.api.artifacts.result.ResolvedComponentResult;
52+
import org.gradle.api.artifacts.result.ResolvedDependencyResult;
53+
import org.gradle.api.file.RegularFileProperty;
54+
import org.gradle.api.provider.MapProperty;
55+
import org.gradle.api.provider.Property;
56+
import org.gradle.api.tasks.Input;
57+
import org.gradle.api.tasks.Internal;
58+
import org.gradle.api.tasks.OutputFile;
59+
import org.gradle.api.tasks.TaskAction;
60+
61+
import java.io.File;
62+
import java.io.IOException;
63+
import java.nio.file.Path;
64+
import java.util.HashMap;
65+
import java.util.LinkedHashSet;
66+
import java.util.Map;
67+
import java.util.Optional;
68+
import java.util.Set;
69+
70+
/**
71+
* Generates a {@code dynamic-access-metadata.json} file used by the dynamic access tab of the native image
72+
* Build Report. This json file contains the mapping of all classpath entries that exist in the
73+
* {@value #LIBRARY_AND_FRAMEWORK_LIST} to their transitive dependencies.
74+
* <p>
75+
* If {@value #LIBRARY_AND_FRAMEWORK_LIST} doesn't exist in the used release of the
76+
* {@code GraalVM Reachability Metadata} repository, this task does nothing.
77+
* <p>
78+
* The format of the generated JSON file conforms the following
79+
* <a href="https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/dynamic-access-metadata-schema-v1.0.0.json">schema</a>.
80+
*/
81+
public abstract class GenerateDynamicAccessMetadata extends DefaultTask {
82+
private static final String LIBRARY_AND_FRAMEWORK_LIST = "library-and-framework-list.json";
83+
84+
public void setClasspath(Configuration classpath) {
85+
getRuntimeClasspathGraph().set(classpath.getIncoming().getResolutionResult().getRootComponent());
86+
87+
// Build coordinates -> path map
88+
Map<String, String> map = new HashMap<>();
89+
for (ResolvedArtifactResult artifact : classpath.getIncoming().getArtifacts().getResolvedArtifacts().get()) {
90+
if (artifact.getId().getComponentIdentifier() instanceof ModuleComponentIdentifier mci) {
91+
String coordinates = mci.getGroup() + ":" + mci.getModule();
92+
map.put(coordinates, artifact.getFile().getAbsolutePath());
93+
}
94+
}
95+
getCoordinatesToPath().set(map);
96+
}
97+
98+
@Input
99+
public abstract Property<ResolvedComponentResult> getRuntimeClasspathGraph();
100+
101+
@Input
102+
public abstract MapProperty<String, String> getCoordinatesToPath();
103+
104+
@Internal
105+
public abstract Property<GraalVMReachabilityMetadataService> getMetadataService();
106+
107+
@OutputFile
108+
public abstract RegularFileProperty getOutputJson();
109+
110+
@TaskAction
111+
public void generate() {
112+
Optional<Path> repositoryDirectory = getMetadataService().get().getRepositoryDirectory();
113+
if (repositoryDirectory.isEmpty()) {
114+
GraalVMLogger.of(getLogger())
115+
.log("No reachability metadata repository is configured or available.");
116+
return;
117+
}
118+
File jsonFile = repositoryDirectory.get().resolve(LIBRARY_AND_FRAMEWORK_LIST).toFile();
119+
if (!jsonFile.exists()) {
120+
GraalVMLogger.of(getLogger())
121+
.log("{} is not packaged with the provided reachability metadata repository.", LIBRARY_AND_FRAMEWORK_LIST);
122+
return;
123+
}
124+
125+
try {
126+
Set<String> artifactsToInclude = DynamicAccessMetadataUtils.readArtifacts(jsonFile);
127+
128+
ResolvedComponentResult root = getRuntimeClasspathGraph().get();
129+
130+
Map<String, Set<String>> exportMap = buildExportMap(root, artifactsToInclude, getCoordinatesToPath().get());
131+
132+
serializeExportMap(getOutputJson().getAsFile().get(), exportMap);
133+
} catch (IOException e) {
134+
GraalVMLogger.of(getLogger()).log("Failed to generate dynamic access metadata: {}", e);
135+
}
136+
}
137+
138+
/**
139+
* Builds a mapping from each entry in the classpath, whose corresponding artifact
140+
* exists in the {@value #LIBRARY_AND_FRAMEWORK_LIST} file, to the set of all of its
141+
* transitive dependency entry paths.
142+
*/
143+
private Map<String, Set<String>> buildExportMap(ResolvedComponentResult root, Set<String> artifactsToInclude, Map<String, String> coordinatesToPath) {
144+
Map<String, Set<String>> exportMap = new HashMap<>();
145+
Map<String, Set<String>> dependencyMap = new HashMap<>();
146+
147+
collectDependencies(root, dependencyMap, new LinkedHashSet<>(), coordinatesToPath);
148+
149+
for (Map.Entry<String, Set<String>> entry : dependencyMap.entrySet()) {
150+
String coordinates = entry.getKey();
151+
if (artifactsToInclude.contains(coordinates)) {
152+
String absolutePath = coordinatesToPath.get(coordinates);
153+
if (absolutePath != null) {
154+
exportMap.put(absolutePath, entry.getValue());
155+
}
156+
}
157+
}
158+
159+
return exportMap;
160+
}
161+
162+
/**
163+
* Recursively collects all classpath entry paths for the given dependency and its transitive dependencies.
164+
*/
165+
private void collectDependencies(ResolvedComponentResult node, Map<String, Set<String>> dependencyMap, Set<String> visited, Map<String, String> coordinatesToPath) {
166+
String coordinates = null;
167+
if (node.getId() instanceof ModuleComponentIdentifier mci) {
168+
coordinates = mci.getGroup() + ":" + mci.getModule();
169+
}
170+
171+
if (coordinates != null && !visited.add(coordinates)) {
172+
return;
173+
}
174+
175+
Set<String> dependencies = new LinkedHashSet<>();
176+
for (DependencyResult dep : node.getDependencies()) {
177+
if (dep instanceof ResolvedDependencyResult resolved) {
178+
ResolvedComponentResult target = resolved.getSelected();
179+
180+
if (target.getId() instanceof ModuleComponentIdentifier targetMci) {
181+
String dependencyCoordinates = targetMci.getGroup() + ":" + targetMci.getModule();
182+
String dependencyPath = coordinatesToPath.get(dependencyCoordinates);
183+
184+
if (dependencyPath != null) {
185+
dependencies.add(dependencyPath);
186+
}
187+
188+
collectDependencies(target, dependencyMap, visited, coordinatesToPath);
189+
190+
Set<String> transitiveDependencies = dependencyMap.get(dependencyCoordinates);
191+
if (transitiveDependencies != null) {
192+
dependencies.addAll(transitiveDependencies);
193+
}
194+
}
195+
}
196+
}
197+
dependencyMap.put(coordinates, dependencies);
198+
}
199+
200+
private void serializeExportMap(File outputFile, Map<String, Set<String>> exportMap) throws IOException {
201+
DynamicAccessMetadataUtils.serialize(outputFile, exportMap);
202+
GraalVMLogger.of(getLogger()).lifecycle("Dynamic Access Metadata written into " + outputFile);
203+
}
204+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ protected void populateClasspath() throws MojoExecutionException {
437437
addDependenciesToClasspath();
438438
}
439439
addInferredDependenciesToClasspath();
440+
maybeAddDynamicAccessMetadataToClasspath();
440441
imageClasspath.removeIf(entry -> !entry.toFile().exists());
441442
}
442443

@@ -543,7 +544,11 @@ protected void maybeAddGeneratedResourcesConfig(List<String> into) {
543544
}
544545
}
545546

546-
547+
protected void maybeAddDynamicAccessMetadataToClasspath() {
548+
if (Files.exists(Path.of(outputDirectory.getPath() ,"dynamic-access-metadata.json"))) {
549+
imageClasspath.add(Path.of(outputDirectory.getPath() ,"dynamic-access-metadata.json"));
550+
}
551+
}
547552

548553
protected void maybeAddReachabilityMetadata(List<String> configDirs) {
549554
if (isMetadataRepositoryEnabled() && !metadataRepositoryConfigurations.isEmpty()) {

0 commit comments

Comments
 (0)