Skip to content

Commit f328d3a

Browse files
authored
Layers DSL follow-up: configuration-cache and test cleanup (#1034)
* Fix layer DSL configuration-cache isolation * Make layered test fixture edits line-ending independent * Make Maven layer configuration descriptor optional * Link layer regression tests to issues
1 parent 8ae8060 commit f328d3a

11 files changed

Lines changed: 225 additions & 41 deletions

File tree

native-gradle-plugin/docs/e2e.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ or Native Image invocation, add or update a functional test in the closest scena
9595
Configuration-cache functional tests validate that Gradle-specific task and provider wiring works
9696
with Gradle's configuration-cache model. They protect [§REQ-gradle-model](requirements.md#req-gradle-model-the-gradle-plugin-preserves-gradle-model-compatibility)
9797
and the architecture boundaries in [§AR-gradle-plugin.2](architecture.md#2-extension-and-option-model) and [§AR-gradle-plugin.3](architecture.md#3-task-graph-architecture).
98+
They cover both named-layer consumers and ordinary native tasks with no declared layers so the
99+
layer DSL cannot leak project model objects into otherwise unrelated task state. [§FS-plugin-model.2](functional/plugin-model.md#2-extension-surface).
98100

99101
Run the configuration-cache suite with the task exposed by the Gradle functional-testing
100102
convention, or let CI run the generated matrix from [§root/AR-repository-ci.1.3](../../docs/spec/architecture/ci.md#13-gradle-plugin-pr-workflow).

native-gradle-plugin/docs/functional/plugin-model.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ name to the same selection set. A consuming binary may select each logical name
6969
plugin resolves provider-backed names lazily and fails duplicate-name validation before any
7070
selected producer task executes. This layer model must not introduce avoidable project or task
7171
container serialization; general configuration-cache compatibility remains governed by the
72-
plugin's existing configuration-cache baseline.
72+
plugin's existing configuration-cache baseline. Binaries that select no named layers retain no
73+
reference to the layer container, its callbacks, or the project model; selected layers reach tasks
74+
only through normalized names, output-file providers, and file collections.
7375

7476
`fromConfiguration(...)` and `all` include resolved external and project dependencies. Dependency
7577
selectors also accept Gradle providers, including version-catalog accessors.

native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/LayeredApplicationFunctionalTest.groovy

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import org.graalvm.buildtools.gradle.fixtures.GraalVMSupport
4646
import org.graalvm.buildtools.utils.NativeImageUtils
4747
import spock.lang.Ignore
4848
import spock.lang.IgnoreIf
49+
import spock.lang.Issue
4950
import spock.lang.Requires
5051
import spock.util.concurrent.PollingConditions
5152

@@ -77,6 +78,47 @@ class LayeredApplicationFunctionalTest extends AbstractFunctionalTest {
7778
outputContains "Builds the dependencies Native Image layer."
7879
}
7980

81+
@Issue("https://github.com/graalvm/native-build-tools/issues/1027")
82+
def "native tasks without layers reuse the configuration cache"() {
83+
given:
84+
withSample("java-application")
85+
buildFile << '''
86+
// Keep the compile task in the graph without invoking Native Image. §FS-plugin-model.2.
87+
tasks.named("nativeCompile") {
88+
enabled = false
89+
}
90+
'''.stripIndent()
91+
92+
when:
93+
runAndReloadConfigurationCache 'nativeCompile', 'generateResourcesConfigFile'
94+
if (hasConfigurationCache) {
95+
// TestKit creates its exploded plugin classpath during the first pair of builds.
96+
// Run once more after that input stabilizes to verify an actual cache reuse.
97+
runAndReloadConfigurationCache 'nativeCompile', 'generateResourcesConfigFile'
98+
}
99+
100+
then:
101+
if (hasConfigurationCache) {
102+
configurationCacheStoreTasks {
103+
upToDate ':generateResourcesConfigFile'
104+
skipped ':nativeCompile'
105+
doesNotContain ':nativeDependenciesLayer'
106+
}
107+
tasks {
108+
upToDate ':generateResourcesConfigFile'
109+
skipped ':nativeCompile'
110+
doesNotContain ':nativeDependenciesLayer'
111+
}
112+
outputContains 'Reusing configuration cache.'
113+
} else {
114+
tasks {
115+
succeeded ':generateResourcesConfigFile'
116+
skipped ':nativeCompile'
117+
doesNotContain ':nativeDependenciesLayer'
118+
}
119+
}
120+
}
121+
80122
def "rejects an empty named layer before invoking Native Image"() {
81123
given:
82124
withSample("layered-java-application")
@@ -91,11 +133,13 @@ class LayeredApplicationFunctionalTest extends AbstractFunctionalTest {
91133
errorOutputContains "Layer 'empty' has no contents"
92134
}
93135

136+
@Issue("https://github.com/graalvm/native-build-tools/issues/1033")
94137
def "rejects duplicate provider layer selections before the producer runs"() {
95138
given:
96139
withSample("layered-java-application")
140+
buildFile.text = buildFile.text.replace('\r\n', '\n').replace('\n', '\r\n')
97141
// Replace the sample's happy-path consumer so this test isolates direct/provider duplicates. §FS-plugin-model.2.
98-
buildFile.text = buildFile.text.replace(''' main {
142+
replaceBuildFile(''' main {
99143
usesLayer('dependencies')
100144
}
101145
''', ''' main {
@@ -270,7 +314,7 @@ public class Application {
270314
implementation("org.slf4j:slf4j-api:2.0.17")
271315
}
272316
'''.stripIndent()
273-
buildFile.text = buildFile.text.replace(''' modules("java.base")
317+
replaceBuildFile(''' modules("java.base")
274318
''', ''' packages("org.slf4j")
275319
''')
276320
buildFile << '''
@@ -300,7 +344,7 @@ public class Application {
300344

301345
given:
302346
withSample("layered-java-application")
303-
buildFile.text = buildFile.text.replace(''' modules("java.base")
347+
replaceBuildFile(''' modules("java.base")
304348
''', ''' all = true
305349
''')
306350
buildFile << '''
@@ -334,7 +378,7 @@ public class Application {
334378
implementation("org.apache.commons:commons-lang3:3.17.0")
335379
}
336380
'''.stripIndent()
337-
buildFile.text = buildFile.text.replace(''' modules("java.base")
381+
replaceBuildFile(''' modules("java.base")
338382
''', ''' modules("java.base")
339383
from(configurations.runtimeClasspath)
340384
''')
@@ -585,4 +629,10 @@ public class Application {
585629
}
586630
}
587631
}
632+
633+
private void replaceBuildFile(String expected, String replacement) {
634+
String normalized = buildFile.text.replace('\r\n', '\n')
635+
assert normalized.contains(expected): "Expected layered application fixture text was not found:\n${expected}"
636+
buildFile.text = normalized.replace(expected, replacement)
637+
}
588638
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.graalvm.buildtools.gradle.internal.GraalVMReachabilityMetadataService;
5858
import org.graalvm.buildtools.gradle.internal.GradleUtils;
5959
import org.graalvm.buildtools.gradle.internal.NativeImageExecutableLocator;
60+
import org.graalvm.buildtools.gradle.internal.NativeImageLayerRegistry;
6061
import org.graalvm.buildtools.gradle.internal.agent.AgentConfigurationFactory;
6162
import org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask;
6263
import org.graalvm.buildtools.gradle.tasks.CollectReachabilityMetadata;
@@ -200,6 +201,7 @@ public class NativeImagePlugin implements Plugin<Project> {
200201
private static final String NATIVE_IMAGE_OPTIONS_ENV = "NATIVE_IMAGE_OPTIONS";
201202

202203
private GraalVMLogger logger;
204+
private transient NativeImageLayerRegistry layerRegistry;
203205

204206
// Exposed detection provider for test binaries (to be used by follow-up tasks)
205207
private Provider<Boolean> compatModeEnabled;
@@ -580,7 +582,7 @@ private void configureLayerTasks(Project project,
580582
project.getObjects(),
581583
project.getProviders(),
582584
project.getExtensions().findByType(JavaToolchainService.class),
583-
graalExtension.getLayers(),
585+
layerRegistry,
584586
"lib" + layer.getName()
585587
);
586588
CreateLayerOptions create = project.getObjects().newInstance(CreateLayerOptions.class);
@@ -910,10 +912,14 @@ private static boolean isJar(FileSystemLocation location) {
910912
}
911913

912914
private GraalVMExtension registerGraalVMExtension(Project project) {
915+
layerRegistry = new NativeImageLayerRegistry(project.getObjects());
913916
NamedDomainObjectContainer<NativeImageLayer> layers = project.getObjects()
914917
.domainObjectContainer(NativeImageLayer.class, name -> {
915918
NativeImageLayerArguments.validateLayerName(name);
916-
return project.getObjects().newInstance(NativeImageLayer.class, name, project.getObjects(), project);
919+
NativeImageLayer layer = project.getObjects().newInstance(
920+
NativeImageLayer.class, name, project.getObjects(), project, layerRegistry);
921+
layerRegistry.register(layer);
922+
return layer;
917923
});
918924
NamedDomainObjectContainer<NativeImageOptions> nativeImages = project.getObjects()
919925
.domainObjectContainer(NativeImageOptions.class, name ->
@@ -922,7 +928,7 @@ private GraalVMExtension registerGraalVMExtension(Project project) {
922928
project.getObjects(),
923929
project.getProviders(),
924930
project.getExtensions().findByType(JavaToolchainService.class),
925-
layers,
931+
layerRegistry,
926932
project.getName())
927933
);
928934
GraalVMExtension graalvmNative = project.getExtensions().create(GraalVMExtension.class, "graalvmNative",

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
import org.gradle.api.model.ObjectFactory;
4949
import org.gradle.api.provider.ListProperty;
5050
import org.gradle.api.provider.Property;
51-
import org.gradle.api.provider.Provider;
5251
import org.gradle.api.tasks.Input;
5352
import org.gradle.api.tasks.Internal;
5453
import org.gradle.api.tasks.Nested;
54+
import org.graalvm.buildtools.gradle.internal.NativeImageLayerRegistry;
5555
import org.graalvm.buildtools.utils.NativeImageLayerArguments;
5656

5757
import javax.inject.Inject;
@@ -64,17 +64,17 @@
6464
public abstract class NativeImageLayer implements Named {
6565
private final String name;
6666
private final LayerContents contents;
67-
private final Project project;
67+
private final transient NativeImageLayerRegistry layerRegistry;
6868
private final ConfigurableFileCollection layerFiles;
6969
private final ConfigurableFileCollection inheritedCompatibilityClasspath;
7070
private final ConfigurableFileCollection compatibilityClasspath;
7171
private final Set<String> configuredLayerNames = new HashSet<>();
7272

7373
@Inject
74-
public NativeImageLayer(String name, ObjectFactory objects, Project project) {
74+
public NativeImageLayer(String name, ObjectFactory objects, Project project, NativeImageLayerRegistry layerRegistry) {
7575
this.name = name;
7676
this.contents = objects.newInstance(LayerContents.class, project);
77-
this.project = project;
77+
this.layerRegistry = layerRegistry;
7878
this.layerFiles = project.files();
7979
this.inheritedCompatibilityClasspath = project.files();
8080
this.compatibilityClasspath = project.files();
@@ -114,12 +114,9 @@ public void usesLayer(String name) {
114114
throw new IllegalArgumentException("Layer '" + name + "' is selected more than once");
115115
}
116116
getUseLayerNames().add(name);
117-
Provider<NativeImageLayer> layer = project.provider(() -> project.getExtensions()
118-
.getByType(GraalVMExtension.class)
119-
.getLayers()
120-
.getByName(name));
121-
layerFiles.from(layer.flatMap(NativeImageLayer::getOutputFile));
122-
inheritedCompatibilityClasspath.from(layer.map(NativeImageLayer::getCompatibilityClasspath));
117+
NativeImageLayerRegistry.LayerReference reference = layerRegistry.reference(name);
118+
layerFiles.from(reference.getOutputFile());
119+
inheritedCompatibilityClasspath.from(reference.getCompatibilityClasspath());
123120
}
124121

125122
@Internal

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
import org.graalvm.buildtools.utils.NativeImageLayerArguments;
5454
import org.gradle.api.Action;
5555
import org.gradle.api.DomainObjectSet;
56-
import org.gradle.api.NamedDomainObjectContainer;
5756
import org.gradle.api.file.ConfigurableFileCollection;
5857
import org.gradle.api.file.DirectoryProperty;
5958
import org.gradle.api.file.ProjectLayout;
@@ -94,13 +93,13 @@ public abstract class BaseNativeImageOptions implements NativeImageOptions {
9493
private static final GraalVMLogger LOGGER = GraalVMLogger.of(Logging.getLogger(BaseNativeImageOptions.class));
9594
private final DomainObjectSet<LayerOptions> layers;
9695
private final ConfigurableFileCollection layerCompatibilityClasspath;
97-
private NativeImageLayer assignedLayer;
96+
private transient NativeImageLayer assignedLayer;
9897

9998
private final String name;
10099
private final transient TaskContainer tasks;
101100
private final ObjectFactory objects;
102101
private final ProviderFactory providers;
103-
private final NamedDomainObjectContainer<NativeImageLayer> namedLayers;
102+
private final transient NativeImageLayerRegistry layerRegistry;
104103

105104
@Override
106105
@Internal
@@ -267,7 +266,7 @@ public BaseNativeImageOptions(String name,
267266
ProviderFactory providers,
268267
JavaToolchainService toolchains,
269268
TaskContainer tasks,
270-
NamedDomainObjectContainer<NativeImageLayer> namedLayers,
269+
NativeImageLayerRegistry layerRegistry,
271270
String defaultImageName) {
272271
this.name = name;
273272
getDebug().convention(false);
@@ -295,7 +294,7 @@ public BaseNativeImageOptions(String name,
295294
this.tasks = tasks;
296295
this.objects = objectFactory;
297296
this.providers = providers;
298-
this.namedLayers = namedLayers;
297+
this.layerRegistry = layerRegistry;
299298
}
300299

301300
private static Provider<Boolean> property(ProviderFactory providers, String name) {
@@ -458,8 +457,7 @@ public void useLayer(String name) {
458457
public void useLayer(NativeImageLayer layer) {
459458
// Typed layer consumption records the producing layer output on the binary. §FS-plugin-model.2.
460459
addLayerName(layer.getName());
461-
getLayerFiles().from(layer.getOutputFile());
462-
layerCompatibilityClasspath.from(layer.getCompatibilityClasspath());
460+
addLayerReference(layerRegistry.reference(layer.getName()));
463461
}
464462

465463
@Override
@@ -472,12 +470,10 @@ public void useLayer(Provider<? extends NativeImageLayer> layer) {
472470

473471
@Override
474472
public void usesLayer(String name) {
475-
// Resolve the container only when Gradle queries the binary, allowing layers to be declared later. §FS-plugin-model.2.
473+
// Resolve normalized properties without retaining the layer container in task state. §FS-plugin-model.2.
476474
NativeImageLayerArguments.validateLayerName(name);
477-
Provider<NativeImageLayer> layer = providers.provider(() -> namedLayers.getByName(name));
478475
addLayerName(name);
479-
getLayerFiles().from(layer.flatMap(NativeImageLayer::getOutputFile));
480-
layerCompatibilityClasspath.from(layer.map(NativeImageLayer::getCompatibilityClasspath));
476+
addLayerReference(layerRegistry.reference(name));
481477
}
482478

483479
@Override
@@ -490,8 +486,14 @@ public void setLayer(NativeImageLayer layer) {
490486
// Singular property assignment replaces any prior layer selection. §FS-plugin-model.2.
491487
assignedLayer = layer;
492488
getLayerNames().set(List.of(layer.getName()));
493-
getLayerFiles().setFrom(layer.getOutputFile());
494-
layerCompatibilityClasspath.setFrom(layer.getCompatibilityClasspath());
489+
NativeImageLayerRegistry.LayerReference reference = layerRegistry.reference(layer.getName());
490+
getLayerFiles().setFrom(reference.getOutputFile());
491+
layerCompatibilityClasspath.setFrom(reference.getCompatibilityClasspath());
492+
}
493+
494+
private void addLayerReference(NativeImageLayerRegistry.LayerReference reference) {
495+
getLayerFiles().from(reference.getOutputFile());
496+
layerCompatibilityClasspath.from(reference.getCompatibilityClasspath());
495497
}
496498

497499
private void addLayerName(String layerName) {

0 commit comments

Comments
 (0)