Skip to content

Commit 95512d0

Browse files
jormundur00vjovanovclaude
authored
Add listLibrariesMissingMetadata task/goal for Gradle and Maven (#877)
* Introduce listMissingMetadataLibs gradle task and maven goal * Never cache gradle listMissingMetadataLibs task * Attempt to fetch gh authentication when it's present * Remove brittle, hard-coded tests * Move from testing fork ticket opening, to actual upstream oracle repo * Fix checkstyle in listMissingMetadataLibs task * Rename listMissingMetadataLibs to listLibrariesMissingMetadata Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add unit tests for direct-runtime dependency selection - ListLibrariesMissingMetadataTest covers the Gradle path: only direct external module dependencies survive, project components and unresolved entries are filtered, transitive children are not walked. - ListLibrariesMissingMetadataMojoTest covers the Maven path: isDirectRuntimeDependency accepts compile/runtime/null scope at trail length 2, rejects test/provided, transitive trails, missing trail, and project-only trails. Both tests run as plain Spock unit tests and require no GitHub access, so they cover the dependency-selection contract without the brittle live-metadata-repo dependency that the earlier functional tests had. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Group listLibrariesMissingMetadata console output and surface createIssues=true The old output interleaved each missing library's coordinates with a long, heavily URL-encoded prefilled issue link, then trailed a dense seven-counter summary line. The call-to-action for filing issues automatically (createIssues=true) was only reachable by reading the docs. The new rendering: - Groups results by status: "Already requested" (existing open issues), "Requested support for N libraries" (when createIssues succeeded), and a manual "Or request support manually" section for the remaining ones. Errors get their own block at the end. - Places a prominent call-to-action above the manual list that shows the exact re-run command tailored to the build tool (`-PcreateIssues=true` / `-DcreateIssues=true`) and the token resolution order on the next line. - Collapses prefilled issue URLs into numbered footnotes ([1], [E1], [C1]) so library names line up vertically instead of drowning in 200-character URLs. - Trims `&maven_coordinates=...` from the prefilled URL (the title already carries the GAV) and keeps colons readable in the title query value. - Appends `Full report: <path>` as a one-line footer when the plugins pass in the report-file path via the new renderConsoleOutput(reportFilePath) overload. - For all-supported runs, collapses to a single sentence. Tests in MissingMetadataCommandSupportTest are updated to assert on the new grouped output (headline, CTA, token sources, footnote indices) and on the trimmed prefilled URL shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix missing metadata forced version and GitHub host resolution * Fix ASCII-only checkstyle violation in metadata test * Restore prefilled coordinates in metadata issue links * Add schema for missing metadata reports * Clarify missing metadata tip on docs landing page * Fail listLibrariesMissingMetadata when issue creation fails --------- Co-authored-by: Vojin Jovanovic <vojin.jovanovic@oracle.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0dcda78 commit 95512d0

14 files changed

Lines changed: 2406 additions & 4 deletions

File tree

common/graalvm-reachability-metadata/src/main/java/org/graalvm/reachability/MissingMetadataCommandSupport.java

Lines changed: 945 additions & 0 deletions
Large diffs are not rendered by default.

common/graalvm-reachability-metadata/src/test/java/org/graalvm/reachability/MissingMetadataCommandSupportTest.java

Lines changed: 536 additions & 0 deletions
Large diffs are not rendered by default.

docs/src/docs/asciidoc/end-to-end-gradle-guide.adoc

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,17 @@ You can find an extensive list of libraries and frameworks from the Java ecosyst
235235
However, it may happen, that your native image crashes at run time with a missing class or resource.
236236
To address this, start by checking if any required configuration is missing.
237237

238-
The best way to detect missing metadata is by running your native tests.
238+
Run this first to see which direct dependencies are not supported by the reachability metadata repository:
239+
240+
[source,bash, role="multi-language-sample"]
241+
----
242+
./gradlew listLibrariesMissingMetadata
243+
----
244+
245+
The task prints missing direct runtime dependencies, reuses existing reachability-metadata issues when it can, and writes a JSON report to _build/reports/native/list-libraries-missing-metadata.json_.
246+
If you want the task to create GitHub issues automatically instead of printing links, run `./gradlew listLibrariesMissingMetadata -PcreateIssues=true`.
247+
248+
If the command reports missing libraries, fix those first.
239249
Alternatively, you can **identify missing configuration manually** using the following method.
240250

241251
- Pass the `--exact-reachability-metadata` option to the `native-image` tool inside `graalvmNative` block of your _build.gradle_, as shown below:
@@ -443,4 +453,4 @@ All the monitoring and debugging tools https://www.graalvm.org/reference-manual/
443453

444454
=== Learn more
445455

446-
To continue learning, refer to the <<gradle-plugin.adoc#,Gradle plugin documentation>>.
456+
To continue learning, refer to the <<gradle-plugin.adoc#,Gradle plugin documentation>>.

docs/src/docs/asciidoc/end-to-end-maven-guide.adoc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,17 @@ You can find an extensive list of libraries and frameworks from the Java ecosyst
333333
However, it may happen, that your native image crashes at run time with a missing class or resource.
334334
To address this, start by checking if any required configuration is missing.
335335

336-
The best way to detect missing metadata is by running your native tests.
336+
Run this first to see which direct dependencies are not supported by the reachability metadata repository:
337+
338+
[source,bash, role="multi-language-sample"]
339+
----
340+
./mvnw native:list-libraries-missing-metadata
341+
----
342+
343+
The goal prints missing direct runtime dependencies, reuses existing reachability-metadata issues when it can, and writes a JSON report to _target/native/list-libraries-missing-metadata.json_.
344+
If you want the goal to create GitHub issues automatically instead of printing links, run `./mvnw -DcreateIssues=true native:list-libraries-missing-metadata`.
345+
346+
If the command reports missing libraries, fix those first.
337347
Alternatively, you can **identify missing configuration manually** using the following method.
338348

339349
- Pass the `--exact-reachability-metadata` option to the `native-image` tool in _pom.xml_, as shown below:

docs/src/docs/asciidoc/gradle-plugin.adoc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,44 @@ include::../snippets/gradle/groovy/build.gradle[tags=specify-metadata-version-fo
825825
include::../snippets/gradle/kotlin/build.gradle.kts[tags=specify-metadata-version-for-library]
826826
----
827827

828+
[[gradle-list-libraries-missing-metadata]]
829+
=== Checking Missing Metadata Support
830+
831+
Run this first to see which direct dependencies are not supported by the reachability metadata repository.
832+
833+
[source,bash, role="multi-language-sample"]
834+
----
835+
./gradlew listLibrariesMissingMetadata
836+
----
837+
838+
By default, the task:
839+
840+
- Scans direct external runtime dependencies from the main runtime classpath
841+
- Uses the configured metadata repository as the source of truth
842+
- Prints existing reachability-metadata issues when they already exist
843+
- Prints prefilled issue links when a dependency is missing metadata support
844+
- Writes a JSON report to _build/reports/native/list-libraries-missing-metadata.json_
845+
846+
The JSON report includes a `$schema` field pointing to _schemas/list-libraries-missing-metadata-schema-v1.0.0.json_ in this repository.
847+
848+
To create issues automatically instead of printing links, opt in explicitly:
849+
850+
[source,bash, role="multi-language-sample"]
851+
----
852+
./gradlew listLibrariesMissingMetadata -PcreateIssues=true
853+
----
854+
855+
You can also override the GitHub integration inputs with Gradle properties such as `targetRepository`, `githubApiUrl`, `githubToken`, and `reportFile`.
856+
857+
The GitHub token used for issue lookup and creation is resolved in this order:
858+
859+
1. The `-PgithubToken=...` Gradle property
860+
2. The `GITHUB_TOKEN` environment variable
861+
3. The `GH_TOKEN` environment variable
862+
4. `gh auth token`, when the GitHub CLI is installed and authenticated (subprocess capped at 5 seconds; non-zero exit is treated as no token)
863+
864+
In default list mode the task tolerates a missing token: it can still query public issues, and emits a single warning if the GitHub API rejects the lookup. With `-PcreateIssues=true`, a token is required.
865+
828866
=== Including Metadata Repository Files
829867

830868
By default, reachability metadata is used only when your native image is being generated.

docs/src/docs/asciidoc/index.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ Currently, you can build your native applications using our Gradle or Maven plug
77
Each plugin takes metadata for available libraries from the https://github.com/oracle/graalvm-reachability-metadata[GraalVM reachability metadata repository]
88
to provide better experience during the development of your native project.
99

10+
[TIP]
11+
====
12+
Check library support first.
13+
Run `./gradlew listLibrariesMissingMetadata` or `./mvnw native:list-libraries-missing-metadata`
14+
before troubleshooting metadata manually so you can see which direct dependencies are unsupported
15+
by the reachability metadata repository.
16+
====
17+
1018
If you are using alternative build systems, see <<alternative-build-systems.adoc#,Useful Hints for Alternative Build Systems>>.
1119

1220
[discrete]

docs/src/docs/asciidoc/maven-plugin.adoc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,44 @@ This may be interesting if there is no specific metadata available for the parti
842842
include::../../../../samples/native-config-integration/pom.xml[tag=metadata-force-version]
843843
----
844844

845+
[[maven-list-libraries-missing-metadata]]
846+
=== Checking Missing Metadata Support
847+
848+
Run this first to see which direct dependencies are not supported by the reachability metadata repository.
849+
850+
[source,bash, role="multi-language-sample"]
851+
----
852+
./mvnw native:list-libraries-missing-metadata
853+
----
854+
855+
By default, the goal:
856+
857+
- Scans direct external runtime dependencies
858+
- Uses the configured metadata repository as the source of truth
859+
- Prints existing reachability-metadata issues when they already exist
860+
- Prints prefilled issue links when a dependency is missing metadata support
861+
- Writes a JSON report to _target/native/list-libraries-missing-metadata.json_
862+
863+
The JSON report includes a `$schema` field pointing to _schemas/list-libraries-missing-metadata-schema-v1.0.0.json_ in this repository.
864+
865+
To create issues automatically instead of printing links, opt in explicitly:
866+
867+
[source,bash, role="multi-language-sample"]
868+
----
869+
./mvnw -DcreateIssues=true -DgithubToken=... native:list-libraries-missing-metadata
870+
----
871+
872+
You can also override the GitHub integration inputs with `targetRepository`, `githubApiUrl`, `githubToken`, and `reportFile`.
873+
874+
The GitHub token used for issue lookup and creation is resolved in this order:
875+
876+
1. The `-DgithubToken=...` system property
877+
2. The `GITHUB_TOKEN` environment variable
878+
3. The `GH_TOKEN` environment variable
879+
4. `gh auth token`, when the GitHub CLI is installed and authenticated (subprocess capped at 5 seconds; non-zero exit is treated as no token)
880+
881+
In default list mode the goal tolerates a missing token: it can still query public issues, and emits a single warning if the GitHub API rejects the lookup. With `-DcreateIssues=true`, a token is required.
882+
845883
=== Adding Metadata Repository Files
846884

847885
By default, reachability metadata is used only when your native image is being generated.

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.graalvm.buildtools.gradle.tasks.CollectReachabilityMetadata;
6262
import org.graalvm.buildtools.gradle.tasks.GenerateDynamicAccessMetadata;
6363
import org.graalvm.buildtools.gradle.tasks.GenerateResourcesConfigFile;
64+
import org.graalvm.buildtools.gradle.tasks.ListLibrariesMissingMetadata;
6465
import org.graalvm.buildtools.gradle.tasks.MetadataCopyTask;
6566
import org.graalvm.buildtools.gradle.tasks.NativeRunTask;
6667
import org.graalvm.buildtools.gradle.tasks.UseLayerOptions;
@@ -71,6 +72,7 @@
7172
import org.graalvm.buildtools.utils.JUnitUtils;
7273
import org.graalvm.buildtools.utils.SharedConstants;
7374
import org.graalvm.reachability.DirectoryConfiguration;
75+
import org.graalvm.reachability.MissingMetadataCommandSupport;
7476
import org.gradle.api.Action;
7577
import org.gradle.api.NamedDomainObjectContainer;
7678
import org.gradle.api.Plugin;
@@ -326,6 +328,35 @@ private void configureJavaProject(Project project, Provider<NativeImageService>
326328
task.getModuleToConfigVersion().convention(metadataRepositoryExtension.getModuleToConfigVersion());
327329
task.getInto().convention(project.getLayout().getBuildDirectory().dir("native-reachability-metadata"));
328330
});
331+
project.getTasks().register("listLibrariesMissingMetadata", ListLibrariesMissingMetadata.class, task -> {
332+
task.setGroup(LifecycleBasePlugin.BUILD_GROUP);
333+
task.setDescription("Lists direct runtime dependencies that do not have reachability metadata support");
334+
task.setClasspath(project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME));
335+
Provider<GraalVMReachabilityMetadataService> reachabilityMetadataService = graalVMReachabilityMetadataService(
336+
project, metadataRepositoryExtension);
337+
task.getMetadataService().set(reachabilityMetadataService);
338+
task.usesService(reachabilityMetadataService);
339+
task.getMetadataRepositoryEnabled().convention(metadataRepositoryExtension.getEnabled());
340+
task.getMetadataRepositoryUri().convention(metadataRepositoryExtension.getUri().map(URI::toString));
341+
task.getCreateIssues().convention(project.getProviders().gradleProperty("createIssues").map(Boolean::parseBoolean).orElse(false));
342+
task.getGithubToken().convention(project.getProviders().gradleProperty("githubToken"));
343+
task.getTargetRepository().convention(
344+
project.getProviders().gradleProperty("targetRepository")
345+
.orElse(MissingMetadataCommandSupport.DEFAULT_TARGET_REPOSITORY)
346+
);
347+
task.getGithubApiUrl().convention(
348+
project.getProviders().gradleProperty("githubApiUrl")
349+
.orElse(MissingMetadataCommandSupport.DEFAULT_GITHUB_API_URL)
350+
);
351+
task.getProjectName().convention(project.getName());
352+
task.getExcludedModules().convention(metadataRepositoryExtension.getExcludedModules());
353+
task.getModuleToConfigVersion().convention(metadataRepositoryExtension.getModuleToConfigVersion());
354+
task.getReportFile().convention(
355+
project.getProviders().gradleProperty("reportFile")
356+
.map(path -> project.getLayout().getProjectDirectory().file(path))
357+
.orElse(project.getLayout().getBuildDirectory().file("reports/native/list-libraries-missing-metadata.json"))
358+
);
359+
});
329360
}
330361

331362
private void configureAutomaticTaskCreation(Project project,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright (c) 2026, 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.GraalVMReachabilityMetadataService;
44+
import org.graalvm.reachability.MissingMetadataCommandSupport;
45+
import org.gradle.api.DefaultTask;
46+
import org.gradle.api.GradleException;
47+
import org.gradle.api.artifacts.Configuration;
48+
import org.gradle.api.artifacts.ModuleVersionIdentifier;
49+
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
50+
import org.gradle.api.artifacts.result.DependencyResult;
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.provider.SetProperty;
57+
import org.gradle.api.tasks.Input;
58+
import org.gradle.api.tasks.Internal;
59+
import org.gradle.api.tasks.Optional;
60+
import org.gradle.api.tasks.OutputFile;
61+
import org.gradle.api.tasks.TaskAction;
62+
63+
import java.io.IOException;
64+
import java.nio.charset.StandardCharsets;
65+
import java.nio.file.Files;
66+
import java.util.ArrayList;
67+
import java.util.Collections;
68+
import java.util.List;
69+
70+
public abstract class ListLibrariesMissingMetadata extends DefaultTask {
71+
public ListLibrariesMissingMetadata() {
72+
getOutputs().upToDateWhen(task -> false);
73+
}
74+
75+
public void setClasspath(Configuration classpath) {
76+
getRootComponent().set(classpath.getIncoming().getResolutionResult().getRootComponent());
77+
}
78+
79+
@Internal
80+
protected abstract Property<ResolvedComponentResult> getRootComponent();
81+
82+
@Internal
83+
public abstract Property<GraalVMReachabilityMetadataService> getMetadataService();
84+
85+
@Input
86+
public abstract Property<Boolean> getMetadataRepositoryEnabled();
87+
88+
@Input
89+
@Optional
90+
public abstract Property<String> getMetadataRepositoryUri();
91+
92+
@Input
93+
public abstract Property<Boolean> getCreateIssues();
94+
95+
@Internal
96+
public abstract Property<String> getGithubToken();
97+
98+
@Input
99+
public abstract Property<String> getTargetRepository();
100+
101+
@Input
102+
public abstract Property<String> getGithubApiUrl();
103+
104+
@Input
105+
public abstract Property<String> getProjectName();
106+
107+
@Input
108+
@Optional
109+
public abstract SetProperty<String> getExcludedModules();
110+
111+
@Input
112+
@Optional
113+
public abstract MapProperty<String, String> getModuleToConfigVersion();
114+
115+
@OutputFile
116+
public abstract RegularFileProperty getReportFile();
117+
118+
@TaskAction
119+
void listLibrariesMissingMetadata() throws IOException {
120+
if (!Boolean.TRUE.equals(getMetadataRepositoryEnabled().get())) {
121+
throw new GradleException("GraalVM reachability metadata repository is disabled.");
122+
}
123+
if (!getRootComponent().isPresent()) {
124+
throw new GradleException("Runtime classpath resolution result is unavailable.");
125+
}
126+
GraalVMReachabilityMetadataService service = getMetadataService().get();
127+
List<MissingMetadataCommandSupport.DependencyCoordinate> dependencies = directExternalRuntimeDependencies(getRootComponent().get());
128+
MissingMetadataCommandSupport.Report report = MissingMetadataCommandSupport.run(
129+
dependencies,
130+
service,
131+
getExcludedModules().getOrElse(Collections.emptySet()),
132+
getModuleToConfigVersion().getOrElse(Collections.emptyMap()),
133+
new MissingMetadataCommandSupport.Options(
134+
"gradle",
135+
getProjectName().get(),
136+
getMetadataRepositoryUri().getOrNull(),
137+
Boolean.TRUE.equals(getCreateIssues().getOrElse(false)),
138+
getGithubToken().getOrNull(),
139+
getTargetRepository().get(),
140+
getGithubApiUrl().get(),
141+
null,
142+
message -> getLogger().warn(message)
143+
)
144+
);
145+
String reportFilePath = getReportFile().get().getAsFile().getAbsolutePath();
146+
writeReport(report.toJsonString());
147+
getLogger().lifecycle(report.renderConsoleOutput(reportFilePath));
148+
}
149+
150+
static List<MissingMetadataCommandSupport.DependencyCoordinate> directExternalRuntimeDependencies(ResolvedComponentResult rootComponent) {
151+
List<MissingMetadataCommandSupport.DependencyCoordinate> dependencies = new ArrayList<>();
152+
for (DependencyResult dependency : rootComponent.getDependencies()) {
153+
if (dependency instanceof ResolvedDependencyResult) {
154+
ResolvedComponentResult selected = ((ResolvedDependencyResult) dependency).getSelected();
155+
if (selected.getId() instanceof ModuleComponentIdentifier) {
156+
ModuleVersionIdentifier moduleVersion = selected.getModuleVersion();
157+
dependencies.add(new MissingMetadataCommandSupport.DependencyCoordinate(
158+
moduleVersion.getGroup(),
159+
moduleVersion.getName(),
160+
moduleVersion.getVersion()
161+
));
162+
}
163+
}
164+
}
165+
return dependencies;
166+
}
167+
168+
private void writeReport(String reportJson) throws IOException {
169+
if (getReportFile().get().getAsFile().getParentFile() != null) {
170+
Files.createDirectories(getReportFile().get().getAsFile().getParentFile().toPath());
171+
}
172+
Files.writeString(getReportFile().get().getAsFile().toPath(), reportJson, StandardCharsets.UTF_8);
173+
}
174+
}

0 commit comments

Comments
 (0)