Skip to content

Commit 37fab88

Browse files
authored
Fix additive reachability metadata requires selection (#1018)
1 parent c300d9f commit 37fab88

27 files changed

Lines changed: 633 additions & 158 deletions

File tree

common/docs/functional-spec.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ metadata directory using index data, module-to-config-version overrides, default
115115
exclusions. Queries must classify dependencies as supported, excluded, not-for-Native-Image, or
116116
missing so plugin diagnostics and reports do not duplicate repository rules.
117117

118+
The artifacts submitted in one query form the eligible resolved dependency graph for single-level
119+
`requires` expansion. Every selected configuration retains the group and artifact identity of the
120+
module whose repository directory owns it. If a required module is present in the submitted graph,
121+
only that module's direct query selects its metadata, using its resolved version, exclusion, and
122+
module-to-config-version override. If it is absent, the requiring module may select it once from the
123+
required module's directory using the requiring module's resolved version and normal exact-version
124+
then latest/default fallback behavior. An indirect required selection counts as repository coverage
125+
for the requiring query, but it does not recursively expand the required module's own `requires`.
126+
118127
### 5.2 Plugin entry points and outputs
119128

120129
Product-specific repository resolution entry points are specified by [§gradle/FS-resources-and-metadata.3](../../native-gradle-plugin/docs/functional/resources-and-metadata.md#3-reachability-metadata-collection)

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

Lines changed: 102 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,19 @@
4747
import org.graalvm.reachability.internal.index.artifacts.SingleModuleJsonVersionToConfigDirectoryIndex;
4848
import org.graalvm.reachability.internal.index.artifacts.VersionToConfigDirectoryIndex;
4949
import org.graalvm.reachability.internal.index.modules.FileSystemModuleToConfigDirectoryIndex;
50+
import org.graalvm.reachability.internal.index.modules.ModuleConfigurationDirectory;
5051

5152
import java.nio.file.Path;
53+
import java.util.Collections;
54+
import java.util.LinkedHashMap;
55+
import java.util.LinkedHashSet;
56+
import java.util.List;
5257
import java.util.Map;
5358
import java.util.Optional;
5459
import java.util.Set;
5560
import java.util.concurrent.ConcurrentHashMap;
5661
import java.util.function.Consumer;
5762
import java.util.function.Supplier;
58-
import java.util.stream.Collectors;
5963

6064
/**
6165
* Queries an unpacked reachability metadata repository. §FS-common-libraries.5.
@@ -99,90 +103,109 @@ public static boolean isSupportedArchiveFormat(String path) {
99103
public Set<DirectoryConfiguration> findConfigurationsFor(Consumer<? super Query> queryBuilder) {
100104
DefaultQuery query = new DefaultQuery();
101105
queryBuilder.accept(query);
102-
return query.getArtifacts()
103-
.stream()
104-
.flatMap(artifactQuery -> {
105-
String groupId = artifactQuery.getGroupId();
106-
String artifactId = artifactQuery.getArtifactId();
107-
String version = artifactQuery.getVersion();
108-
return moduleIndex.findConfigurationDirectories(groupId, artifactId)
109-
.stream()
110-
.map(dir -> {
111-
VersionToConfigDirectoryIndex index = artifactIndexes.computeIfAbsent(dir,
112-
SingleModuleJsonVersionToConfigDirectoryIndex::new);
113-
if (artifactQuery.getForcedConfig().isPresent()) {
114-
String configVersion = artifactQuery.getForcedConfig().get();
115-
logger.log(groupId, artifactId, version, "Configuration is forced to version " + configVersion);
116-
return index.findConfiguration(groupId, artifactId, configVersion);
117-
}
118-
Optional<DirectoryConfiguration> configuration = index.findConfiguration(groupId, artifactId, version);
119-
if (!configuration.isPresent() && artifactQuery.isUseLatestVersion()) {
120-
logger.log(groupId, artifactId, version, "Configuration directory not found. Trying latest version.");
121-
configuration = index.findLatestConfigurationFor(groupId, artifactId, version);
122-
if (!configuration.isPresent()) {
123-
logger.log(groupId, artifactId, version, "Latest version not found!");
124-
}
125-
}
126-
Optional<DirectoryConfiguration> finalConfigurationDirectory = configuration;
127-
logger.log(groupId, artifactId, version, () -> {
128-
if (finalConfigurationDirectory.isPresent()) {
129-
Path path = finalConfigurationDirectory.get().getDirectory();
130-
return "Configuration directory is " + rootDirectory.relativize(path);
131-
}
132-
return "missing.";
133-
});
134-
return configuration;
135-
})
136-
.filter(Optional::isPresent)
137-
.map(Optional::get);
138-
})
139-
.collect(Collectors.toSet());
106+
List<DefaultArtifactQuery> artifacts = query.getArtifacts();
107+
Set<String> directlyQueriedModules = directlyQueriedModules(artifacts);
108+
Map<String, DirectoryConfiguration> configurations = new LinkedHashMap<>();
109+
// Select the complete query as one graph so requires never preempts a direct module query. §FS-common-libraries.5.1.
110+
for (DefaultArtifactQuery artifactQuery : artifacts) {
111+
for (ModuleConfigurationDirectory candidate : moduleIndex.findConfigurationDirectories(
112+
artifactQuery.getGroupId(), artifactQuery.getArtifactId())) {
113+
boolean direct = isSameModule(candidate, artifactQuery);
114+
if (!direct && directlyQueriedModules.contains(moduleKey(candidate.getGroupId(), candidate.getArtifactId()))) {
115+
continue;
116+
}
117+
Optional<String> forcedConfig = direct ? artifactQuery.getForcedConfig() : Optional.empty();
118+
selectConfiguration(candidate, artifactQuery.getVersion(), artifactQuery.isUseLatestVersion(), forcedConfig)
119+
.ifPresent(configuration -> configurations.putIfAbsent(configurationKey(configuration), configuration));
120+
}
121+
}
122+
return Collections.unmodifiableSet(new LinkedHashSet<>(configurations.values()));
140123
}
141124

142125
@Override
143126
public boolean isCoveredByRepository(Consumer<? super Query> queryBuilder) {
144127
DefaultQuery query = new DefaultQuery();
145128
queryBuilder.accept(query);
146-
return query.getArtifacts()
147-
.stream()
148-
.anyMatch(artifactQuery -> {
149-
String groupId = artifactQuery.getGroupId();
150-
String artifactId = artifactQuery.getArtifactId();
151-
String version = artifactQuery.getVersion();
152-
return moduleIndex.findConfigurationDirectories(groupId, artifactId)
153-
.stream()
154-
.anyMatch(dir -> {
155-
VersionToConfigDirectoryIndex index = artifactIndexes.computeIfAbsent(dir, SingleModuleJsonVersionToConfigDirectoryIndex::new);
156-
Optional<DirectoryConfiguration> configuration;
157-
if (artifactQuery.getForcedConfig().isPresent()) {
158-
String configVersion = artifactQuery.getForcedConfig().get();
159-
logger.log(groupId, artifactId, version, "Configuration is forced to version " + configVersion);
160-
configuration = index.findConfiguration(groupId, artifactId, configVersion);
161-
} else {
162-
configuration = index.findConfiguration(groupId, artifactId, version);
163-
if (!configuration.isPresent() && artifactQuery.isUseLatestVersion()) {
164-
logger.log(groupId, artifactId, version,
165-
"Configuration directory not found. Trying latest version.");
166-
configuration = index.findLatestConfigurationFor(groupId, artifactId, version);
167-
if (!configuration.isPresent()) {
168-
logger.log(groupId, artifactId, version, "Latest version not found!");
169-
}
170-
}
171-
}
172-
if (configuration.isPresent()) {
173-
Path path = configuration.get().getDirectory();
174-
logger.log(groupId, artifactId, version,
175-
"Configuration directory is " + rootDirectory.relativize(path));
176-
return true;
177-
}
178-
if (index.isNotForNativeImage(groupId, artifactId, version)) {
179-
logger.log(groupId, artifactId, version, "Artifact is marked as not for native-image.");
180-
return true;
181-
}
182-
logger.log(groupId, artifactId, version, "missing.");
183-
return false;
184-
});
185-
});
129+
List<DefaultArtifactQuery> artifacts = query.getArtifacts();
130+
Set<String> directlyQueriedModules = directlyQueriedModules(artifacts);
131+
// Check the complete query as one graph so requires never preempts a direct module query. §FS-common-libraries.5.1.
132+
for (DefaultArtifactQuery artifactQuery : artifacts) {
133+
for (ModuleConfigurationDirectory candidate : moduleIndex.findConfigurationDirectories(
134+
artifactQuery.getGroupId(), artifactQuery.getArtifactId())) {
135+
boolean direct = isSameModule(candidate, artifactQuery);
136+
if (!direct && directlyQueriedModules.contains(moduleKey(candidate.getGroupId(), candidate.getArtifactId()))) {
137+
continue;
138+
}
139+
Optional<String> forcedConfig = direct ? artifactQuery.getForcedConfig() : Optional.empty();
140+
if (isCovered(candidate, artifactQuery.getVersion(), artifactQuery.isUseLatestVersion(), forcedConfig)) {
141+
return true;
142+
}
143+
}
144+
}
145+
return false;
146+
}
147+
148+
private Optional<DirectoryConfiguration> selectConfiguration(ModuleConfigurationDirectory candidate,
149+
String version, boolean useLatestVersion, Optional<String> forcedConfig) {
150+
String groupId = candidate.getGroupId();
151+
String artifactId = candidate.getArtifactId();
152+
VersionToConfigDirectoryIndex index = artifactIndexes.computeIfAbsent(candidate.getDirectory(),
153+
SingleModuleJsonVersionToConfigDirectoryIndex::new);
154+
Optional<DirectoryConfiguration> configuration;
155+
if (forcedConfig.isPresent()) {
156+
String configVersion = forcedConfig.get();
157+
logger.log(groupId, artifactId, version, "Configuration is forced to version " + configVersion);
158+
configuration = index.findConfiguration(groupId, artifactId, configVersion);
159+
} else {
160+
configuration = index.findConfiguration(groupId, artifactId, version);
161+
if (!configuration.isPresent() && useLatestVersion) {
162+
logger.log(groupId, artifactId, version, "Configuration directory not found. Trying latest version.");
163+
configuration = index.findLatestConfigurationFor(groupId, artifactId, version);
164+
if (!configuration.isPresent()) {
165+
logger.log(groupId, artifactId, version, "Latest version not found!");
166+
}
167+
}
168+
}
169+
Optional<DirectoryConfiguration> result = configuration;
170+
logger.log(groupId, artifactId, version, () -> result
171+
.map(value -> "Configuration directory is " + rootDirectory.relativize(value.getDirectory()))
172+
.orElse("missing."));
173+
return configuration;
174+
}
175+
176+
private boolean isCovered(ModuleConfigurationDirectory candidate, String version, boolean useLatestVersion,
177+
Optional<String> forcedConfig) {
178+
Optional<DirectoryConfiguration> configuration = selectConfiguration(candidate, version, useLatestVersion, forcedConfig);
179+
if (configuration.isPresent()) {
180+
return true;
181+
}
182+
VersionToConfigDirectoryIndex index = artifactIndexes.get(candidate.getDirectory());
183+
if (index.isNotForNativeImage(candidate.getGroupId(), candidate.getArtifactId(), version)) {
184+
logger.log(candidate.getGroupId(), candidate.getArtifactId(), version, "Artifact is marked as not for native-image.");
185+
return true;
186+
}
187+
return false;
188+
}
189+
190+
private static Set<String> directlyQueriedModules(List<DefaultArtifactQuery> artifacts) {
191+
Set<String> modules = new LinkedHashSet<>();
192+
for (DefaultArtifactQuery artifact : artifacts) {
193+
modules.add(moduleKey(artifact.getGroupId(), artifact.getArtifactId()));
194+
}
195+
return modules;
196+
}
197+
198+
private static boolean isSameModule(ModuleConfigurationDirectory candidate, DefaultArtifactQuery artifact) {
199+
return candidate.getGroupId().equals(artifact.getGroupId()) && candidate.getArtifactId().equals(artifact.getArtifactId());
200+
}
201+
202+
private static String moduleKey(String groupId, String artifactId) {
203+
return groupId + ':' + artifactId;
204+
}
205+
206+
private static String configurationKey(DirectoryConfiguration configuration) {
207+
return moduleKey(configuration.getGroupId(), configuration.getArtifactId()) + ':' + configuration.getVersion()
208+
+ ':' + configuration.getDirectory().normalize() + ':' + configuration.isOverride();
186209
}
187210

188211
public Path getRootDirectory() {

common/graalvm-reachability-metadata/src/main/java/org/graalvm/reachability/internal/index/modules/FileSystemModuleToConfigDirectoryIndex.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
* Module-to-config index which:
5656
* - Resolves the primary module directory by conventional layout (groupId/artifactId),
5757
* - Reads requires from the inner metadata/group/artifact/index.json and adds their conventional directories.
58+
* §FS-common-libraries.5.1.
5859
*/
5960
public class FileSystemModuleToConfigDirectoryIndex implements ModuleToConfigDirectoryIndex {
6061
private final Path rootPath;
@@ -71,18 +72,18 @@ public FileSystemModuleToConfigDirectoryIndex(Path rootPath) {
7172
* - Only a single-level requires expansion is performed
7273
*/
7374
@Override
74-
public Set<Path> findConfigurationDirectories(String groupId, String artifactId) {
75+
public Set<ModuleConfigurationDirectory> findConfigurationDirectories(String groupId, String artifactId) {
7576
Path base = rootPath.resolve(groupId + "/" + artifactId);
7677
if (!Files.isDirectory(base)) {
7778
return Collections.emptySet();
7879
}
7980

8081
Path indexFile = base.resolve("index.json");
8182
if (Files.isRegularFile(indexFile)) {
82-
Set<Path> result = new LinkedHashSet<>();
83+
Set<ModuleConfigurationDirectory> result = new LinkedHashSet<>();
8384
// Always include the base directory so its index.json is parsed,
8485
// even if it doesn't contain configuration files itself.
85-
result.add(base);
86+
result.add(new ModuleConfigurationDirectory(groupId, artifactId, base));
8687
try {
8788
String content = Files.readString(indexFile);
8889
JSONArray entries = new JSONArray(content);
@@ -100,7 +101,7 @@ public Set<Path> findConfigurationDirectories(String groupId, String artifactId)
100101
String reqArtifact = req.substring(sep + 1);
101102
Path reqDir = rootPath.resolve(reqGroup + "/" + reqArtifact);
102103
if (Files.isDirectory(reqDir)) {
103-
result.add(reqDir);
104+
result.add(new ModuleConfigurationDirectory(reqGroup, reqArtifact, reqDir));
104105
}
105106
}
106107
}
@@ -111,6 +112,6 @@ public Set<Path> findConfigurationDirectories(String groupId, String artifactId)
111112
return result;
112113
}
113114

114-
return Collections.singleton(base);
115+
return Collections.singleton(new ModuleConfigurationDirectory(groupId, artifactId, base));
115116
}
116117
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.reachability.internal.index.modules;
42+
43+
import java.nio.file.Path;
44+
import java.util.Objects;
45+
46+
/**
47+
* A candidate metadata directory together with the module identity that owns it.
48+
* §FS-common-libraries.5.1.
49+
*/
50+
public final class ModuleConfigurationDirectory {
51+
private final String groupId;
52+
private final String artifactId;
53+
private final Path directory;
54+
55+
public ModuleConfigurationDirectory(String groupId, String artifactId, Path directory) {
56+
this.groupId = groupId;
57+
this.artifactId = artifactId;
58+
this.directory = directory;
59+
}
60+
61+
public String getGroupId() {
62+
return groupId;
63+
}
64+
65+
public String getArtifactId() {
66+
return artifactId;
67+
}
68+
69+
public Path getDirectory() {
70+
return directory;
71+
}
72+
73+
@Override
74+
public boolean equals(Object other) {
75+
if (this == other) {
76+
return true;
77+
}
78+
if (!(other instanceof ModuleConfigurationDirectory)) {
79+
return false;
80+
}
81+
ModuleConfigurationDirectory that = (ModuleConfigurationDirectory) other;
82+
return groupId.equals(that.groupId) && artifactId.equals(that.artifactId) && directory.equals(that.directory);
83+
}
84+
85+
@Override
86+
public int hashCode() {
87+
return Objects.hash(groupId, artifactId, directory);
88+
}
89+
}

0 commit comments

Comments
 (0)