Skip to content

Commit d4dacd5

Browse files
authored
Implement gradle configuration cache support (#1500)
Modern versions of Gradle support configuration caching to prevent the gradual increase of project size to affect the overall developer experience of Gradle builds. To prepare the PKL project, and specificall pkl-gradle, for configuration support, we introduce an integration test to vet configuration cache rules, and then perform the necessary updates to provide configuration cache support.
1 parent 7b70a44 commit d4dacd5

16 files changed

Lines changed: 698 additions & 439 deletions

pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java

Lines changed: 132 additions & 135 deletions
Large diffs are not rendered by default.

pkl-gradle/src/main/java/org/pkl/gradle/task/AnalyzeImportsTask.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.io.File;
1919
import org.gradle.api.file.RegularFileProperty;
2020
import org.gradle.api.provider.Property;
21-
import org.gradle.api.provider.Provider;
2221
import org.gradle.api.tasks.CacheableTask;
2322
import org.gradle.api.tasks.Input;
2423
import org.gradle.api.tasks.Optional;
@@ -35,20 +34,15 @@ public abstract class AnalyzeImportsTask extends ModulesTask {
3534
@Input
3635
public abstract Property<String> getOutputFormat();
3736

38-
private final Provider<CliImportAnalyzer> cliImportAnalyzerProvider =
39-
getProviders()
40-
.provider(
41-
() ->
42-
new CliImportAnalyzer(
43-
new CliImportAnalyzerOptions(
44-
getCliBaseOptions(),
45-
mapAndGetOrNull(getOutputFile(), it -> it.getAsFile().toPath()),
46-
mapAndGetOrNull(getOutputFormat(), it -> it))));
47-
4837
@Override
4938
protected void doRunTask() {
5039
//noinspection ResultOfMethodCallIgnored
5140
getOutputs().getPreviousOutputFiles().forEach(File::delete);
52-
cliImportAnalyzerProvider.get().run();
41+
new CliImportAnalyzer(
42+
new CliImportAnalyzerOptions(
43+
getCliBaseOptions(),
44+
mapAndGetOrNull(getOutputFile(), it -> it.getAsFile().toPath()),
45+
mapAndGetOrNull(getOutputFormat(), it -> it)))
46+
.run();
5347
}
5448
}

pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ public Provider<URI> getSettingsModuleUri() {
103103
.map((Transformer<@Nullable URI, Object>) object -> object instanceof URI uri ? uri : null);
104104
}
105105

106+
/**
107+
* The working directory for the task, used as the base for relative path resolution. This
108+
* replaces direct access to {@code project.getProjectDir()} at execution time to support the
109+
* Gradle configuration cache.
110+
*/
111+
@Internal
112+
public abstract DirectoryProperty getWorkingDir();
113+
114+
// Exposed as a task input via workingDirPath so that a change to the project directory
115+
// invalidates the task without tracking the directory's contents.
116+
@Input
117+
public Provider<String> getWorkingDirPath() {
118+
return getWorkingDir().map(it -> it.getAsFile().getAbsolutePath());
119+
}
120+
106121
// Exposed as a task input via evalRootDirPath, because we only need to depend
107122
// on this directory's path and not on its contents.
108123
@Internal
@@ -174,42 +189,39 @@ public void runTask() {
174189

175190
protected abstract void doRunTask();
176191

177-
protected @Nullable CliBaseOptions __cachedOptions;
178-
179192
// Must be called during task execution time only.
193+
// Note: CliBaseOptions is intentionally not cached — caching would require holding a reference
194+
// across the configuration/execution boundary, which is incompatible with the Gradle
195+
// configuration cache. The cost of constructing this object per-invocation is negligible.
180196
@Internal
181197
protected CliBaseOptions getCliBaseOptions() {
182-
if (__cachedOptions == null) {
183-
__cachedOptions =
184-
new CliBaseOptions(
185-
getSourceModulesAsUris(),
186-
patternsFromStrings(getAllowedModules().get()),
187-
patternsFromStrings(getAllowedResources().get()),
188-
getEnvironmentVariables().get(),
189-
getExternalProperties().get(),
190-
parseModulePath(),
191-
getProject().getProjectDir().toPath(),
192-
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
193-
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
194-
null,
195-
getEvalTimeout().getOrNull(),
196-
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
197-
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
198-
getNoCache().getOrElse(false),
199-
false,
200-
false,
201-
false,
202-
getTestPort().getOrElse(-1),
203-
Collections.emptyList(),
204-
getHttpProxy().getOrNull(),
205-
getHttpNoProxy().getOrElse(List.of()),
206-
getHttpRewrites().getOrNull(),
207-
Map.of(),
208-
Map.of(),
209-
null,
210-
getPowerAssertions().getOrElse(false));
211-
}
212-
return __cachedOptions;
198+
return new CliBaseOptions(
199+
getSourceModulesAsUris(),
200+
patternsFromStrings(getAllowedModules().get()),
201+
patternsFromStrings(getAllowedResources().get()),
202+
getEnvironmentVariables().get(),
203+
getExternalProperties().get(),
204+
parseModulePath(),
205+
getWorkingDir().get().getAsFile().toPath(),
206+
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
207+
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
208+
null,
209+
getEvalTimeout().getOrNull(),
210+
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
211+
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
212+
getNoCache().getOrElse(false),
213+
false,
214+
false,
215+
false,
216+
getTestPort().getOrElse(-1),
217+
Collections.emptyList(),
218+
getHttpProxy().getOrNull(),
219+
getHttpNoProxy().getOrElse(List.of()),
220+
getHttpRewrites().getOrNull(),
221+
Map.of(),
222+
Map.of(),
223+
null,
224+
getPowerAssertions().getOrElse(false));
213225
}
214226

215227
@Internal

pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import org.gradle.api.file.FileCollection;
2424
import org.gradle.api.file.RegularFileProperty;
2525
import org.gradle.api.provider.Property;
26-
import org.gradle.api.provider.Provider;
2726
import org.gradle.api.tasks.CacheableTask;
2827
import org.gradle.api.tasks.Input;
2928
import org.gradle.api.tasks.Internal;
@@ -56,35 +55,34 @@ public abstract class EvalTask extends ModulesTask {
5655
@Optional
5756
public abstract Property<String> getExpression();
5857

59-
private final Provider<CliEvaluator> cliEvaluator =
60-
getProviders()
61-
.provider(
62-
() ->
63-
new CliEvaluator(
64-
new CliEvaluatorOptions(
65-
getCliBaseOptions(),
66-
getOutputFile().get().getAsFile().getAbsolutePath(),
67-
getOutputFormat().get(),
68-
getModuleOutputSeparator().get(),
69-
mapAndGetOrNull(
70-
getMultipleFileOutputDir(), it -> it.getAsFile().getAbsolutePath()),
71-
getExpression().getOrNull())));
58+
private CliEvaluator createCliEvaluator() {
59+
return new CliEvaluator(
60+
new CliEvaluatorOptions(
61+
getCliBaseOptions(),
62+
getOutputFile().get().getAsFile().getAbsolutePath(),
63+
getOutputFormat().get(),
64+
getModuleOutputSeparator().get(),
65+
mapAndGetOrNull(getMultipleFileOutputDir(), it -> it.getAsFile().getAbsolutePath()),
66+
getExpression().getOrNull()));
67+
}
7268

7369
@SuppressWarnings("unused")
7470
@OutputFiles
7571
@Optional
7672
public FileCollection getEffectiveOutputFiles() {
7773
return getObjects()
7874
.fileCollection()
79-
.from(cliEvaluator.map(e -> nullToEmpty(e.getOutputFiles())));
75+
.from(getProviders().provider(() -> nullToEmpty(createCliEvaluator().getOutputFiles())));
8076
}
8177

8278
@OutputDirectories
8379
@Optional
8480
public FileCollection getEffectiveOutputDirs() {
8581
return getObjects()
8682
.fileCollection()
87-
.from(cliEvaluator.map(e -> nullToEmpty(e.getOutputDirectories())));
83+
.from(
84+
getProviders()
85+
.provider(() -> nullToEmpty(createCliEvaluator().getOutputDirectories())));
8886
}
8987

9088
private static <T> Set<T> nullToEmpty(@Nullable Set<T> set) {
@@ -95,6 +93,6 @@ private static <T> Set<T> nullToEmpty(@Nullable Set<T> set) {
9593
protected void doRunTask() {
9694
//noinspection ResultOfMethodCallIgnored
9795
getOutputs().getPreviousOutputFiles().forEach(File::delete);
98-
cliEvaluator.get().run();
96+
createCliEvaluator().run();
9997
}
10098
}

pkl-gradle/src/main/java/org/pkl/gradle/task/JavaCodeGenTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected void doRunTask() {
4747
new CliJavaCodeGenerator(
4848
new CliJavaCodeGeneratorOptions(
4949
getCliBaseOptions(),
50-
getProject().file(getOutputDir()).toPath(),
50+
getOutputDir().get().getAsFile().toPath(),
5151
getIndent().get(),
5252
getAddGeneratedAnnotation().get(),
5353
getGenerateGetters().get(),

pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ protected void doRunTask() {
3535
new CliKotlinCodeGenerator(
3636
new CliKotlinCodeGeneratorOptions(
3737
getCliBaseOptions(),
38-
getProject().file(getOutputDir()).toPath(),
38+
getOutputDir().get().getAsFile().toPath(),
3939
getIndent().get(),
4040
getGenerateKdoc().get(),
4141
getGenerateSpringBootConfig().get(),

pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.nio.file.Paths;
2121
import java.util.ArrayList;
2222
import java.util.Collections;
23-
import java.util.HashMap;
2423
import java.util.List;
2524
import java.util.Map;
2625
import java.util.stream.Collectors;
@@ -56,21 +55,17 @@ public abstract class ModulesTask extends BasePklTask {
5655
@PathSensitive(PathSensitivity.ABSOLUTE)
5756
public abstract ListProperty<File> getTransitiveModules();
5857

59-
private final Map<List<Object>, Pair<List<File>, List<URI>>> parsedSourceModulesCache =
60-
new HashMap<>();
61-
6258
// Used for input tracking purposes only.
6359
@Internal
6460
public Provider<Pair<List<File>, List<URI>>> getParsedSourceModules() {
65-
return getSourceModules()
66-
.map(it -> parsedSourceModulesCache.computeIfAbsent(it, this::splitFilesAndUris));
61+
return getSourceModules().map(this::splitFilesAndUris);
6762
}
6863

6964
// We use @InputFiles and FileCollection here to ensure that file contents are tracked.
7065
@InputFiles
7166
@PathSensitive(PathSensitivity.ABSOLUTE)
7267
public FileCollection getSourceModuleFiles() {
73-
return getProject().files(getParsedSourceModules().map(it -> it.first));
68+
return getObjects().fileCollection().from(getParsedSourceModules().map(it -> it.first));
7469
}
7570

7671
// We use @Input and just a list value because we can only track the URIs themselves
@@ -144,37 +139,34 @@ public void runTask() {
144139

145140
@Internal
146141
@Override
142+
// See BasePklTask.getCliBaseOptions() for why caching is intentionally omitted.
147143
protected CliBaseOptions getCliBaseOptions() {
148-
if (__cachedOptions == null) {
149-
__cachedOptions =
150-
new CliBaseOptions(
151-
getSourceModulesAsUris(),
152-
patternsFromStrings(getAllowedModules().get()),
153-
patternsFromStrings(getAllowedResources().get()),
154-
getEnvironmentVariables().get(),
155-
getExternalProperties().get(),
156-
parseModulePath(),
157-
getProject().getProjectDir().toPath(),
158-
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
159-
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
160-
getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null,
161-
getEvalTimeout().getOrNull(),
162-
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
163-
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
164-
getNoCache().getOrElse(false),
165-
getOmitProjectSettings().getOrElse(false),
166-
getNoProject().getOrElse(false),
167-
false,
168-
getTestPort().getOrElse(-1),
169-
Collections.emptyList(),
170-
null,
171-
List.of(),
172-
getHttpRewrites().getOrNull(),
173-
Map.of(),
174-
Map.of(),
175-
null,
176-
getPowerAssertions().getOrElse(false));
177-
}
178-
return __cachedOptions;
144+
return new CliBaseOptions(
145+
getSourceModulesAsUris(),
146+
patternsFromStrings(getAllowedModules().get()),
147+
patternsFromStrings(getAllowedResources().get()),
148+
getEnvironmentVariables().get(),
149+
getExternalProperties().get(),
150+
parseModulePath(),
151+
getWorkingDir().get().getAsFile().toPath(),
152+
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
153+
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
154+
getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null,
155+
getEvalTimeout().getOrNull(),
156+
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
157+
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
158+
getNoCache().getOrElse(false),
159+
getOmitProjectSettings().getOrElse(false),
160+
getNoProject().getOrElse(false),
161+
false,
162+
getTestPort().getOrElse(-1),
163+
Collections.emptyList(),
164+
null,
165+
List.of(),
166+
getHttpRewrites().getOrNull(),
167+
Map.of(),
168+
Map.of(),
169+
null,
170+
getPowerAssertions().getOrElse(false));
179171
}
180172
}

pkl-gradle/src/main/java/org/pkl/gradle/utils/PluginUtils.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,11 +19,16 @@
1919
import java.net.URI;
2020
import java.net.URISyntaxException;
2121
import java.net.URL;
22+
import java.nio.file.Files;
2223
import java.nio.file.InvalidPathException;
2324
import java.nio.file.Path;
2425
import java.nio.file.Paths;
26+
import java.util.Collections;
27+
import java.util.List;
2528
import org.gradle.api.InvalidUserDataException;
2629
import org.gradle.api.file.FileSystemLocation;
30+
import org.gradle.api.file.RegularFile;
31+
import org.pkl.core.ImportGraph;
2732
import org.pkl.core.util.IoUtils;
2833

2934
public class PluginUtils {
@@ -125,4 +130,34 @@ public static URI parseModuleNotationToUri(Object m) {
125130
var parsed1 = PluginUtils.parseModuleNotation(m);
126131
return parsedModuleNotationToUri(parsed1);
127132
}
133+
134+
/**
135+
* Parses the list of file-scheme transitive imports from the JSON output file produced by an
136+
* analyze imports task. Returns an empty list if the file does not exist. Throws a {@link
137+
* RuntimeException} if the file exists but cannot be read or parsed, so that upstream errors are
138+
* not silently lost.
139+
*
140+
* <p>The automatically-created {@code GatherImports} tasks always write JSON, so this method
141+
* assumes JSON format and should only be called for those tasks.
142+
*
143+
* @param outputFile the output file produced by the analyze imports task
144+
* @return the list of file-based transitive import paths
145+
*/
146+
public static List<File> parseTransitiveFiles(RegularFile outputFile) {
147+
if (!outputFile.getAsFile().exists()) {
148+
return Collections.emptyList();
149+
}
150+
try {
151+
var contents = Files.readString(outputFile.getAsFile().toPath());
152+
var importGraph = ImportGraph.parseFromJson(contents);
153+
var imports = importGraph.resolvedImports().values();
154+
return imports.stream()
155+
.filter(it -> it.getScheme().equalsIgnoreCase("file"))
156+
.map(File::new)
157+
.toList();
158+
} catch (Exception e) {
159+
throw new RuntimeException(
160+
"Failed to parse transitive imports from " + outputFile.getAsFile(), e);
161+
}
162+
}
128163
}

0 commit comments

Comments
 (0)