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.
This commit is contained in:
Lucas Shadler
2026-04-20 22:02:13 -07:00
committed by GitHub
parent 7b70a44272
commit d4dacd5a0f
16 changed files with 695 additions and 436 deletions

View File

@@ -16,8 +16,6 @@
package org.pkl.gradle;
import java.io.File;
import java.nio.file.Files;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
@@ -38,7 +36,6 @@ import org.gradle.plugins.ide.idea.model.IdeaModel;
import org.gradle.util.GradleVersion;
import org.jspecify.annotations.Nullable;
import org.pkl.cli.CliEvaluatorOptions;
import org.pkl.core.ImportGraph;
import org.pkl.core.OutputFormat;
import org.pkl.core.util.IoUtils;
import org.pkl.gradle.spec.AnalyzeImportsSpec;
@@ -70,45 +67,38 @@ public class PklPlugin implements Plugin<Project> {
private static final String MIN_GRADLE_VERSION = "8.2";
private @Nullable Project __project;
@Override
public void apply(Project project) {
__project = project;
if (GradleVersion.current().compareTo(GradleVersion.version(MIN_GRADLE_VERSION)) < 0) {
throw new GradleException(
String.format("Plugin `org.pkl` requires Gradle %s or higher.", MIN_GRADLE_VERSION));
}
var extension = project().getExtensions().create("pkl", PklExtension.class);
configureExtension(extension);
var extension = project.getExtensions().create("pkl", PklExtension.class);
configureExtension(project, extension);
}
private Project project() {
assert __project != null;
return __project;
private void configureExtension(Project project, PklExtension extension) {
configureEvalTasks(project, extension.getEvaluators());
configureJavaCodeGenTasks(project, extension.getJavaCodeGenerators());
configureKotlinCodeGenTasks(project, extension.getKotlinCodeGenerators());
configurePkldocTasks(project, extension.getPkldocGenerators());
configureTestTasks(project, extension.getTests());
configureProjectPackageTasks(project, extension.getProject().getPackagers());
configureProjectResolveTasks(project, extension.getProject().getResolvers());
configureAnalyzeImportsTasks(project, extension.getAnalyzers().getImports());
}
private void configureExtension(PklExtension extension) {
configureEvalTasks(extension.getEvaluators());
configureJavaCodeGenTasks(extension.getJavaCodeGenerators());
configureKotlinCodeGenTasks(extension.getKotlinCodeGenerators());
configurePkldocTasks(extension.getPkldocGenerators());
configureTestTasks(extension.getTests());
configureProjectPackageTasks(extension.getProject().getPackagers());
configureProjectResolveTasks(extension.getProject().getResolvers());
configureAnalyzeImportsTasks(extension.getAnalyzers().getImports());
}
private void configureProjectPackageTasks(NamedDomainObjectContainer<ProjectPackageSpec> specs) {
private void configureProjectPackageTasks(
Project project, NamedDomainObjectContainer<ProjectPackageSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureBaseSpec(project, spec);
spec.getOutputPath()
.convention(project().getLayout().getBuildDirectory().dir("generated/pkl/packages"));
.convention(project.getLayout().getBuildDirectory().dir("generated/pkl/packages"));
spec.getOverwrite().convention(false);
var packageTask = createTask(ProjectPackageTask.class, spec);
var packageTask = createTask(project, ProjectPackageTask.class, spec);
packageTask.configure(
task -> {
task.getProjectDirectories().from(spec.getProjectDirectories());
@@ -117,12 +107,12 @@ public class PklPlugin implements Plugin<Project> {
task.getJunitReportsDir().set(spec.getJunitReportsDir());
task.getOverwrite().set(spec.getOverwrite());
});
project()
project
.getPluginManager()
.withPlugin(
"base",
appliedPlugin ->
project()
project
.getTasks()
.named(
LifecycleBasePlugin.BUILD_TASK_NAME,
@@ -130,38 +120,40 @@ public class PklPlugin implements Plugin<Project> {
});
}
private void configureProjectResolveTasks(NamedDomainObjectContainer<ProjectResolveSpec> specs) {
private void configureProjectResolveTasks(
Project project, NamedDomainObjectContainer<ProjectResolveSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
var resolveTask = createTask(ProjectResolveTask.class, spec);
configureBaseSpec(project, spec);
var resolveTask = createTask(project, ProjectResolveTask.class, spec);
resolveTask.configure(
task -> task.getProjectDirectories().from(spec.getProjectDirectories()));
});
}
private void configureAnalyzeImportsTasks(NamedDomainObjectContainer<AnalyzeImportsSpec> specs) {
private void configureAnalyzeImportsTasks(
Project project, NamedDomainObjectContainer<AnalyzeImportsSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureBaseSpec(project, spec);
spec.getOutputFormat().convention(OutputFormat.PCF.toString());
var analyzeImportsTask = createTask(AnalyzeImportsTask.class, spec);
var analyzeImportsTask = createTask(project, AnalyzeImportsTask.class, spec);
analyzeImportsTask.configure(
task -> {
task.getOutputFormat().set(spec.getOutputFormat());
task.getOutputFile().set(spec.getOutputFile());
configureModulesTask(task, spec, null);
configureModulesTask(project, task, spec, null);
});
});
}
private void configureEvalTasks(NamedDomainObjectContainer<EvalSpec> specs) {
private void configureEvalTasks(Project project, NamedDomainObjectContainer<EvalSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureBaseSpec(project, spec);
spec.getOutputFile()
.convention(
project()
project
.getLayout()
.getProjectDirectory()
// %{moduleDir} is resolved relatively to the working directory,
@@ -174,7 +166,7 @@ public class PklPlugin implements Plugin<Project> {
spec.getExpression()
.convention(CliEvaluatorOptions.Companion.getDefaults().getExpression());
createModulesTask(EvalTask.class, spec)
createModulesTask(project, EvalTask.class, spec)
.configure(
task -> {
task.getOutputFile().set(spec.getOutputFile());
@@ -186,11 +178,12 @@ public class PklPlugin implements Plugin<Project> {
});
}
private void configureJavaCodeGenTasks(NamedDomainObjectContainer<JavaCodeGenSpec> specs) {
private void configureJavaCodeGenTasks(
Project project, NamedDomainObjectContainer<JavaCodeGenSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureCodeGenSpec(spec);
configureBaseSpec(project, spec);
configureCodeGenSpec(project, spec);
spec.getGenerateGetters().convention(false);
spec.getGenerateJavadoc().convention(false);
@@ -198,14 +191,13 @@ public class PklPlugin implements Plugin<Project> {
// constructor parameters annotations by setting this property to `null`.
spec.getParamsAnnotation()
.set(
project()
.provider(
() ->
spec.getGenerateSpringBootConfig().get()
? null
: "org.pkl.config.java.mapper.Named"));
project.provider(
() ->
spec.getGenerateSpringBootConfig().get()
? null
: "org.pkl.config.java.mapper.Named"));
createModulesTask(JavaCodeGenTask.class, spec)
createModulesTask(project, JavaCodeGenTask.class, spec)
.configure(
task -> {
configureCodeGenTask(task, spec);
@@ -216,26 +208,26 @@ public class PklPlugin implements Plugin<Project> {
});
});
project()
.afterEvaluate(
prj ->
specs.all(
spec -> {
configureIdeaModule(spec);
configureCodeGenSpecSourceDirectories(
spec, "java", s -> Optional.of(s.getJava()));
}));
project.afterEvaluate(
prj ->
specs.all(
spec -> {
configureIdeaModule(prj, spec);
configureCodeGenSpecSourceDirectories(
prj, spec, "java", s -> Optional.of(s.getJava()));
}));
}
private void configureKotlinCodeGenTasks(NamedDomainObjectContainer<KotlinCodeGenSpec> specs) {
private void configureKotlinCodeGenTasks(
Project project, NamedDomainObjectContainer<KotlinCodeGenSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureCodeGenSpec(spec);
configureBaseSpec(project, spec);
configureCodeGenSpec(project, spec);
spec.getGenerateKdoc().convention(false);
createModulesTask(KotlinCodeGenTask.class, spec)
createModulesTask(project, KotlinCodeGenTask.class, spec)
.configure(
task -> {
configureCodeGenTask(task, spec);
@@ -243,32 +235,31 @@ public class PklPlugin implements Plugin<Project> {
});
});
project()
.afterEvaluate(
prj ->
specs.all(
spec -> {
configureIdeaModule(spec);
configureCodeGenSpecSourceDirectories(
spec, "kotlin", this::getKotlinSourceDirectorySet);
}));
project.afterEvaluate(
prj ->
specs.all(
spec -> {
configureIdeaModule(project, spec);
configureCodeGenSpecSourceDirectories(
project, spec, "kotlin", this::getKotlinSourceDirectorySet);
}));
}
private void configurePkldocTasks(NamedDomainObjectContainer<PkldocSpec> specs) {
private void configurePkldocTasks(Project project, NamedDomainObjectContainer<PkldocSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureBaseSpec(project, spec);
spec.getOutputDir()
.convention(
project()
project
.getLayout()
.getBuildDirectory()
.map(it -> it.dir("pkldoc").dir(spec.getName())));
spec.getNoSymlinks().convention(false);
createModulesTask(PkldocTask.class, spec)
createModulesTask(project, PkldocTask.class, spec)
.configure(
task -> {
task.getOutputDir().set(spec.getOutputDir());
@@ -277,26 +268,26 @@ public class PklPlugin implements Plugin<Project> {
});
}
private void configureTestTasks(NamedDomainObjectContainer<TestSpec> specs) {
private void configureTestTasks(Project project, NamedDomainObjectContainer<TestSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureBaseSpec(project, spec);
spec.getOverwrite().convention(false);
var testTask = createModulesTask(TestTask.class, spec);
var testTask = createModulesTask(project, TestTask.class, spec);
testTask.configure(
task -> {
task.getJunitReportsDir().set(spec.getJunitReportsDir());
task.getOverwrite().set(spec.getOverwrite());
});
project()
project
.getPluginManager()
.withPlugin(
"base",
appliedPlugin ->
project()
project
.getTasks()
.named(
LifecycleBasePlugin.CHECK_TASK_NAME,
@@ -304,7 +295,7 @@ public class PklPlugin implements Plugin<Project> {
});
}
private void configureBaseSpec(BasePklSpec spec) {
private void configureBaseSpec(Project project, BasePklSpec spec) {
spec.getAllowedModules()
.convention(
List.of(
@@ -313,7 +304,7 @@ public class PklPlugin implements Plugin<Project> {
spec.getAllowedResources()
.convention(List.of("env:", "prop:", "file:", "modulepath:", "https:", "package:"));
spec.getEvalRootDir().convention(project().getRootProject().getLayout().getProjectDirectory());
spec.getEvalRootDir().convention(project.getRootProject().getLayout().getProjectDirectory());
// Defaulting to OS env vars is bad for reproducibility and cachability.
// Hence, this spec defaults to empty env vars, which is consistent with other Gradle tasks
@@ -330,22 +321,21 @@ public class PklPlugin implements Plugin<Project> {
spec.getHttpNoProxy().convention(List.of());
}
private void configureCodeGenSpec(CodeGenSpec spec) {
private void configureCodeGenSpec(Project project, CodeGenSpec spec) {
spec.getOutputDir()
.convention(
project()
project
.getLayout()
.getBuildDirectory()
.map(it -> it.dir("generated").dir("pkl").dir(spec.getName())));
spec.getSourceSet()
.convention(
project()
project
.getProviders()
.provider(
() -> {
var sourceSets =
project().getExtensions().findByType(SourceSetContainer.class);
var sourceSets = project.getExtensions().findByType(SourceSetContainer.class);
if (sourceSets == null) {
return null;
}
@@ -360,10 +350,10 @@ public class PklPlugin implements Plugin<Project> {
spec.getAddGeneratedAnnotation().convention(false);
configureCodeGenSpecModulePath(spec);
configureCodeGenSpecModulePath(project, spec);
}
private void configureCodeGenSpecModulePath(CodeGenSpec spec) {
private void configureCodeGenSpecModulePath(Project project, CodeGenSpec spec) {
// Set module path to all of the configured resources source directories and the compile
// classpath to find Pkl modules that are classpath resources.
// Compilation classpath should be correct (vs. runtime classpath) if the library author
@@ -379,7 +369,7 @@ public class PklPlugin implements Plugin<Project> {
// Refer to configureCodeGenSpecSourceDirectories for logic which links the codegen task
// to sourceSet.getResources().getSourceDirectories().
var modulePath = project().files();
var modulePath = project.files();
modulePath
.from(getResourceSourceDirectoriesExceptSpecOutput(spec))
// This technically breaks the dependency on compile classpath builder tasks,
@@ -412,21 +402,26 @@ public class PklPlugin implements Plugin<Project> {
.getFiles()));
}
// Must be called from Project.afterEvaluate() only, because this method depends
// on user-provided configuration not accessible with lazy configuration.
private void configureCodeGenSpecSourceDirectories(
Project project,
CodeGenSpec spec,
String languageName,
Function<? super SourceSet, ? extends Optional<SourceDirectorySet>>
extractSourceDirectorySet) {
var task = project().getTasks().named(spec.getName(), CodeGenTask.class);
var sourceSet = spec.getSourceSet().get();
var task = project.getTasks().named(spec.getName(), CodeGenTask.class);
var sourceSet = spec.getSourceSet().getOrNull();
if (sourceSet == null) {
throw new GradleException(
"Could not find a source set for code generator '"
+ spec.getName()
+ "'. Either apply a JVM plugin (e.g. 'java') or set the sourceSet property explicitly.");
}
extractSourceDirectorySet
.apply(sourceSet)
.ifPresentOrElse(
dirSet -> dirSet.srcDir(task.flatMap(t -> t.getOutputDir().dir(languageName))),
() ->
project()
project
.getLogger()
.debug(
"Source directory set for language {} is not available, "
@@ -436,18 +431,23 @@ public class PklPlugin implements Plugin<Project> {
sourceSet.getResources().srcDir(task.flatMap(t -> t.getOutputDir().dir("resources")));
}
// Must be called from Project.afterEvaluate() only, because this method depends
// on user-provided configuration not accessible with lazy configuration.
private void configureIdeaModule(CodeGenSpec spec) {
project()
private void configureIdeaModule(Project project, CodeGenSpec spec) {
project
.getPluginManager()
.withPlugin(
"idea",
plugin -> {
var module = project().getExtensions().getByType(IdeaModel.class).getModule();
var module = project.getExtensions().getByType(IdeaModel.class).getModule();
var outputDir = spec.getOutputDir().get().getAsFile();
module.getGeneratedSourceDirs().add(outputDir);
if (spec.getSourceSet().get().getName().toLowerCase().contains("test")) {
var sourceSet = spec.getSourceSet().getOrNull();
if (sourceSet == null) {
throw new GradleException(
"Could not find a source set for code generator '"
+ spec.getName()
+ "'. Either apply a JVM plugin (e.g. 'java') or set the sourceSet property explicitly.");
}
if (sourceSet.getName().toLowerCase().contains("test")) {
module.getTestSources().from(append(module.getTestSources().getFiles(), outputDir));
} else {
module.setSourceDirs(append(module.getSourceDirs(), outputDir));
@@ -464,7 +464,9 @@ public class PklPlugin implements Plugin<Project> {
task.getRenames().set(spec.getRenames());
}
private <T extends BasePklTask, S extends BasePklSpec> void configureBaseTask(T task, S spec) {
private <T extends BasePklTask, S extends BasePklSpec> void configureBaseTask(
Project project, T task, S spec) {
task.getWorkingDir().set(project.getLayout().getProjectDirectory());
task.getAllowedModules().set(spec.getAllowedModules());
task.getAllowedResources().set(spec.getAllowedResources());
task.getEnvironmentVariables().set(spec.getEnvironmentVariables());
@@ -482,30 +484,13 @@ public class PklPlugin implements Plugin<Project> {
task.getHttpRewrites().set(spec.getHttpRewrites());
}
private List<File> getTransitiveModules(AnalyzeImportsTask analyzeTask) {
var outputFile = analyzeTask.getOutputFile().get().getAsFile().toPath();
if (!analyzeTask.getOnlyIf().isSatisfiedBy(analyzeTask)) {
return Collections.emptyList();
}
try {
var contents = Files.readString(outputFile);
ImportGraph importGraph = ImportGraph.parseFromJson(contents);
var imports = importGraph.resolvedImports().values();
return imports.stream()
.filter((it) -> it.getScheme().equalsIgnoreCase("file"))
.map(File::new)
.toList();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private <T extends ModulesTask, S extends ModulesSpec> void configureModulesTask(
Project project,
T task,
S spec,
@Nullable TaskProvider<AnalyzeImportsTask> analyzeImportsTask,
@Nullable Transformer<List<?>, List<?>> mapSourceModules) {
configureBaseTask(task, spec);
configureBaseTask(project, task, spec);
if (mapSourceModules != null) {
task.getSourceModules().set(spec.getSourceModules().map(mapSourceModules));
} else {
@@ -518,28 +503,39 @@ public class PklPlugin implements Plugin<Project> {
task.getTransitiveModules().set(spec.getTransitiveModules());
} else if (analyzeImportsTask != null) {
task.dependsOn(analyzeImportsTask);
task.getTransitiveModules().set(analyzeImportsTask.map(this::getTransitiveModules));
// Map the output file provider to the list of transitive files
// This avoids capturing the task reference directly, making it configuration-cache-safe
task.getTransitiveModules()
.set(
analyzeImportsTask
.flatMap(AnalyzeImportsTask::getOutputFile)
.map(PluginUtils::parseTransitiveFiles));
}
}
private <T extends ModulesTask, S extends ModulesSpec> void configureModulesTask(
T task, S spec, @Nullable TaskProvider<AnalyzeImportsTask> analyzeImportsTask) {
configureModulesTask(task, spec, analyzeImportsTask, null);
Project project,
T task,
S spec,
@Nullable TaskProvider<AnalyzeImportsTask> analyzeImportsTask) {
configureModulesTask(project, task, spec, analyzeImportsTask, null);
}
private TaskProvider<AnalyzeImportsTask> createAnalyzeImportsTask(ModulesSpec spec) {
private TaskProvider<AnalyzeImportsTask> createAnalyzeImportsTask(
Project project, ModulesSpec spec) {
var outputFile =
project()
project
.getLayout()
.getBuildDirectory()
.file("pkl-gradle/imports/" + spec.getName() + ".json");
return project()
return project
.getTasks()
.register(
spec.getName() + "GatherImports",
AnalyzeImportsTask.class,
task -> {
configureModulesTask(
project,
task,
spec,
null,
@@ -573,20 +569,21 @@ public class PklPlugin implements Plugin<Project> {
* needing to manually provide transitive modules.
*/
private <T extends ModulesTask> TaskProvider<T> createModulesTask(
Class<T> taskClass, ModulesSpec spec) {
var analyzeImportsTask = createAnalyzeImportsTask(spec);
return project()
Project project, Class<T> taskClass, ModulesSpec spec) {
var analyzeImportsTask = createAnalyzeImportsTask(project, spec);
return project
.getTasks()
.register(
spec.getName(),
taskClass,
task -> configureModulesTask(task, spec, analyzeImportsTask));
task -> configureModulesTask(project, task, spec, analyzeImportsTask));
}
private <T extends BasePklTask> TaskProvider<T> createTask(Class<T> taskClass, BasePklSpec spec) {
return project()
private <T extends BasePklTask> TaskProvider<T> createTask(
Project project, Class<T> taskClass, BasePklSpec spec) {
return project
.getTasks()
.register(spec.getName(), taskClass, task -> configureBaseTask(task, spec));
.register(spec.getName(), taskClass, task -> configureBaseTask(project, task, spec));
}
private <T> Set<T> append(Set<? extends T> set1, T element) {

View File

@@ -18,7 +18,6 @@ package org.pkl.gradle.task;
import java.io.File;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
@@ -35,20 +34,15 @@ public abstract class AnalyzeImportsTask extends ModulesTask {
@Input
public abstract Property<String> getOutputFormat();
private final Provider<CliImportAnalyzer> cliImportAnalyzerProvider =
getProviders()
.provider(
() ->
new CliImportAnalyzer(
new CliImportAnalyzerOptions(
getCliBaseOptions(),
mapAndGetOrNull(getOutputFile(), it -> it.getAsFile().toPath()),
mapAndGetOrNull(getOutputFormat(), it -> it))));
@Override
protected void doRunTask() {
//noinspection ResultOfMethodCallIgnored
getOutputs().getPreviousOutputFiles().forEach(File::delete);
cliImportAnalyzerProvider.get().run();
new CliImportAnalyzer(
new CliImportAnalyzerOptions(
getCliBaseOptions(),
mapAndGetOrNull(getOutputFile(), it -> it.getAsFile().toPath()),
mapAndGetOrNull(getOutputFormat(), it -> it)))
.run();
}
}

View File

@@ -103,6 +103,21 @@ public abstract class BasePklTask extends DefaultTask {
.map((Transformer<@Nullable URI, Object>) object -> object instanceof URI uri ? uri : null);
}
/**
* The working directory for the task, used as the base for relative path resolution. This
* replaces direct access to {@code project.getProjectDir()} at execution time to support the
* Gradle configuration cache.
*/
@Internal
public abstract DirectoryProperty getWorkingDir();
// Exposed as a task input via workingDirPath so that a change to the project directory
// invalidates the task without tracking the directory's contents.
@Input
public Provider<String> getWorkingDirPath() {
return getWorkingDir().map(it -> it.getAsFile().getAbsolutePath());
}
// Exposed as a task input via evalRootDirPath, because we only need to depend
// on this directory's path and not on its contents.
@Internal
@@ -174,42 +189,39 @@ public abstract class BasePklTask extends DefaultTask {
protected abstract void doRunTask();
protected @Nullable CliBaseOptions __cachedOptions;
// Must be called during task execution time only.
// Note: CliBaseOptions is intentionally not cached — caching would require holding a reference
// across the configuration/execution boundary, which is incompatible with the Gradle
// configuration cache. The cost of constructing this object per-invocation is negligible.
@Internal
protected CliBaseOptions getCliBaseOptions() {
if (__cachedOptions == null) {
__cachedOptions =
new CliBaseOptions(
getSourceModulesAsUris(),
patternsFromStrings(getAllowedModules().get()),
patternsFromStrings(getAllowedResources().get()),
getEnvironmentVariables().get(),
getExternalProperties().get(),
parseModulePath(),
getProject().getProjectDir().toPath(),
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
null,
getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
getNoCache().getOrElse(false),
false,
false,
false,
getTestPort().getOrElse(-1),
Collections.emptyList(),
getHttpProxy().getOrNull(),
getHttpNoProxy().getOrElse(List.of()),
getHttpRewrites().getOrNull(),
Map.of(),
Map.of(),
null,
getPowerAssertions().getOrElse(false));
}
return __cachedOptions;
return new CliBaseOptions(
getSourceModulesAsUris(),
patternsFromStrings(getAllowedModules().get()),
patternsFromStrings(getAllowedResources().get()),
getEnvironmentVariables().get(),
getExternalProperties().get(),
parseModulePath(),
getWorkingDir().get().getAsFile().toPath(),
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
null,
getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
getNoCache().getOrElse(false),
false,
false,
false,
getTestPort().getOrElse(-1),
Collections.emptyList(),
getHttpProxy().getOrNull(),
getHttpNoProxy().getOrElse(List.of()),
getHttpRewrites().getOrNull(),
Map.of(),
Map.of(),
null,
getPowerAssertions().getOrElse(false));
}
@Internal

View File

@@ -23,7 +23,6 @@ import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
@@ -56,19 +55,16 @@ public abstract class EvalTask extends ModulesTask {
@Optional
public abstract Property<String> getExpression();
private final Provider<CliEvaluator> cliEvaluator =
getProviders()
.provider(
() ->
new CliEvaluator(
new CliEvaluatorOptions(
getCliBaseOptions(),
getOutputFile().get().getAsFile().getAbsolutePath(),
getOutputFormat().get(),
getModuleOutputSeparator().get(),
mapAndGetOrNull(
getMultipleFileOutputDir(), it -> it.getAsFile().getAbsolutePath()),
getExpression().getOrNull())));
private CliEvaluator createCliEvaluator() {
return new CliEvaluator(
new CliEvaluatorOptions(
getCliBaseOptions(),
getOutputFile().get().getAsFile().getAbsolutePath(),
getOutputFormat().get(),
getModuleOutputSeparator().get(),
mapAndGetOrNull(getMultipleFileOutputDir(), it -> it.getAsFile().getAbsolutePath()),
getExpression().getOrNull()));
}
@SuppressWarnings("unused")
@OutputFiles
@@ -76,7 +72,7 @@ public abstract class EvalTask extends ModulesTask {
public FileCollection getEffectiveOutputFiles() {
return getObjects()
.fileCollection()
.from(cliEvaluator.map(e -> nullToEmpty(e.getOutputFiles())));
.from(getProviders().provider(() -> nullToEmpty(createCliEvaluator().getOutputFiles())));
}
@OutputDirectories
@@ -84,7 +80,9 @@ public abstract class EvalTask extends ModulesTask {
public FileCollection getEffectiveOutputDirs() {
return getObjects()
.fileCollection()
.from(cliEvaluator.map(e -> nullToEmpty(e.getOutputDirectories())));
.from(
getProviders()
.provider(() -> nullToEmpty(createCliEvaluator().getOutputDirectories())));
}
private static <T> Set<T> nullToEmpty(@Nullable Set<T> set) {
@@ -95,6 +93,6 @@ public abstract class EvalTask extends ModulesTask {
protected void doRunTask() {
//noinspection ResultOfMethodCallIgnored
getOutputs().getPreviousOutputFiles().forEach(File::delete);
cliEvaluator.get().run();
createCliEvaluator().run();
}
}

View File

@@ -47,7 +47,7 @@ public abstract class JavaCodeGenTask extends CodeGenTask {
new CliJavaCodeGenerator(
new CliJavaCodeGeneratorOptions(
getCliBaseOptions(),
getProject().file(getOutputDir()).toPath(),
getOutputDir().get().getAsFile().toPath(),
getIndent().get(),
getAddGeneratedAnnotation().get(),
getGenerateGetters().get(),

View File

@@ -35,7 +35,7 @@ public abstract class KotlinCodeGenTask extends CodeGenTask {
new CliKotlinCodeGenerator(
new CliKotlinCodeGeneratorOptions(
getCliBaseOptions(),
getProject().file(getOutputDir()).toPath(),
getOutputDir().get().getAsFile().toPath(),
getIndent().get(),
getGenerateKdoc().get(),
getGenerateSpringBootConfig().get(),

View File

@@ -20,7 +20,6 @@ import java.net.URI;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -56,21 +55,17 @@ public abstract class ModulesTask extends BasePklTask {
@PathSensitive(PathSensitivity.ABSOLUTE)
public abstract ListProperty<File> getTransitiveModules();
private final Map<List<Object>, Pair<List<File>, List<URI>>> parsedSourceModulesCache =
new HashMap<>();
// Used for input tracking purposes only.
@Internal
public Provider<Pair<List<File>, List<URI>>> getParsedSourceModules() {
return getSourceModules()
.map(it -> parsedSourceModulesCache.computeIfAbsent(it, this::splitFilesAndUris));
return getSourceModules().map(this::splitFilesAndUris);
}
// We use @InputFiles and FileCollection here to ensure that file contents are tracked.
@InputFiles
@PathSensitive(PathSensitivity.ABSOLUTE)
public FileCollection getSourceModuleFiles() {
return getProject().files(getParsedSourceModules().map(it -> it.first));
return getObjects().fileCollection().from(getParsedSourceModules().map(it -> it.first));
}
// We use @Input and just a list value because we can only track the URIs themselves
@@ -144,37 +139,34 @@ public abstract class ModulesTask extends BasePklTask {
@Internal
@Override
// See BasePklTask.getCliBaseOptions() for why caching is intentionally omitted.
protected CliBaseOptions getCliBaseOptions() {
if (__cachedOptions == null) {
__cachedOptions =
new CliBaseOptions(
getSourceModulesAsUris(),
patternsFromStrings(getAllowedModules().get()),
patternsFromStrings(getAllowedResources().get()),
getEnvironmentVariables().get(),
getExternalProperties().get(),
parseModulePath(),
getProject().getProjectDir().toPath(),
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null,
getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
getNoCache().getOrElse(false),
getOmitProjectSettings().getOrElse(false),
getNoProject().getOrElse(false),
false,
getTestPort().getOrElse(-1),
Collections.emptyList(),
null,
List.of(),
getHttpRewrites().getOrNull(),
Map.of(),
Map.of(),
null,
getPowerAssertions().getOrElse(false));
}
return __cachedOptions;
return new CliBaseOptions(
getSourceModulesAsUris(),
patternsFromStrings(getAllowedModules().get()),
patternsFromStrings(getAllowedResources().get()),
getEnvironmentVariables().get(),
getExternalProperties().get(),
parseModulePath(),
getWorkingDir().get().getAsFile().toPath(),
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null,
getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
getNoCache().getOrElse(false),
getOmitProjectSettings().getOrElse(false),
getNoProject().getOrElse(false),
false,
getTestPort().getOrElse(-1),
Collections.emptyList(),
null,
List.of(),
getHttpRewrites().getOrNull(),
Map.of(),
Map.of(),
null,
getPowerAssertions().getOrElse(false));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,11 +19,16 @@ import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.file.RegularFile;
import org.pkl.core.ImportGraph;
import org.pkl.core.util.IoUtils;
public class PluginUtils {
@@ -125,4 +130,34 @@ public class PluginUtils {
var parsed1 = PluginUtils.parseModuleNotation(m);
return parsedModuleNotationToUri(parsed1);
}
/**
* Parses the list of file-scheme transitive imports from the JSON output file produced by an
* analyze imports task. Returns an empty list if the file does not exist. Throws a {@link
* RuntimeException} if the file exists but cannot be read or parsed, so that upstream errors are
* not silently lost.
*
* <p>The automatically-created {@code GatherImports} tasks always write JSON, so this method
* assumes JSON format and should only be called for those tasks.
*
* @param outputFile the output file produced by the analyze imports task
* @return the list of file-based transitive import paths
*/
public static List<File> parseTransitiveFiles(RegularFile outputFile) {
if (!outputFile.getAsFile().exists()) {
return Collections.emptyList();
}
try {
var contents = Files.readString(outputFile.getAsFile().toPath());
var importGraph = ImportGraph.parseFromJson(contents);
var imports = importGraph.resolvedImports().values();
return imports.stream()
.filter(it -> it.getScheme().equalsIgnoreCase("file"))
.map(File::new)
.toList();
} catch (Exception e) {
throw new RuntimeException(
"Failed to parse transitive imports from " + outputFile.getAsFile(), e);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,20 @@ import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.readString
abstract class AbstractTest {
companion object {
/**
* Gradle output message emitted when the configuration cache is populated for the first time.
* Extracted as a constant so that version-specific phrasing changes only need one update.
*/
const val CONFIG_CACHE_STORED = "Configuration cache entry stored"
/**
* Gradle output message emitted when a prior configuration cache entry is reused. Extracted as
* a constant so that version-specific phrasing changes only need one update.
*/
const val CONFIG_CACHE_REUSED = "Reusing configuration cache"
}
private val gradleVersion: String? = System.getProperty("testGradleVersion")
private val gradleDistributionUrl: String? = System.getProperty("testGradleDistributionUrl")
@@ -56,6 +70,51 @@ abstract class AbstractTest {
}
}
/**
* Runs the given task with the Gradle configuration cache enabled. Uses forked mode
* (withDebug=false) because the configuration cache is not supported in debug/in-process mode.
*
* Runs the task twice: once to populate the configuration cache, and once to verify the cache is
* reused.
*/
protected fun runTaskWithConfigurationCache(taskName: String): Pair<BuildResult, BuildResult> {
fun createRunner(): GradleRunner {
val runner =
GradleRunner.create()
.withProjectDir(testProjectDir.toFile())
.withArguments("--stacktrace", "--no-build-cache", "--configuration-cache", taskName)
.withPluginClasspath()
// Configuration cache requires forked mode (not in-process debug mode)
.withDebug(false)
if (gradleVersion != null) {
runner.withGradleVersion(gradleVersion)
}
if (gradleDistributionUrl != null) {
runner.withGradleDistribution(URI(gradleDistributionUrl))
}
return runner
}
val firstRun =
try {
createRunner().build()
} catch (e: UnexpectedBuildFailure) {
throw AssertionError("Configuration cache: first run failed\n${e.buildResult.output}")
}
val secondRun =
try {
createRunner().build()
} catch (e: UnexpectedBuildFailure) {
throw AssertionError(
"Configuration cache: second run (reuse) failed\n${e.buildResult.output}"
)
}
return Pair(firstRun, secondRun)
}
protected fun writeFile(fileName: String, contents: String): Path {
return testProjectDir.resolve(fileName).apply {
createParentDirectories()

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -41,7 +41,7 @@ class EvaluatorsTest : AbstractTest() {
name = "Pigeon"
age = 30
}
"""
"""
.trimIndent(),
)
}
@@ -61,7 +61,7 @@ class EvaluatorsTest : AbstractTest() {
person:
name: Pigeon
age: 30
"""
"""
.trimIndent(),
)
}
@@ -84,7 +84,7 @@ class EvaluatorsTest : AbstractTest() {
"age": 30
}
}
"""
"""
.trimIndent(),
)
}
@@ -114,7 +114,7 @@ class EvaluatorsTest : AbstractTest() {
</dict>
</dict>
</plist>
"""
"""
.trimIndent(),
)
}
@@ -125,7 +125,7 @@ class EvaluatorsTest : AbstractTest() {
"pcf",
"""
externalProperties = [prop1: "value1", prop2: "value2"]
"""
"""
.trimIndent(),
)
@@ -134,7 +134,7 @@ class EvaluatorsTest : AbstractTest() {
prop1 = read("prop:prop1")
prop2 = read("prop:prop2")
other = read?("prop:other")
"""
"""
.trimIndent()
)
@@ -147,7 +147,7 @@ class EvaluatorsTest : AbstractTest() {
prop1 = "value1"
prop2 = "value2"
other = null
"""
"""
.trimIndent(),
)
}
@@ -161,7 +161,7 @@ class EvaluatorsTest : AbstractTest() {
prop1 = read?("env:USER")
prop2 = read?("env:PATH")
prop3 = read?("env:JAVA_HOME")
"""
"""
.trimIndent()
)
@@ -174,7 +174,7 @@ class EvaluatorsTest : AbstractTest() {
prop1 = null
prop2 = null
prop3 = null
"""
"""
.trimIndent(),
)
}
@@ -185,7 +185,7 @@ class EvaluatorsTest : AbstractTest() {
"pcf",
"""
environmentVariables = [VAR1: "value1", VAR2: "value2"]
"""
"""
.trimIndent(),
)
@@ -194,7 +194,7 @@ class EvaluatorsTest : AbstractTest() {
prop1 = read("env:VAR1")
prop2 = read("env:VAR2")
other = read?("env:OTHER")
"""
"""
.trimIndent()
)
@@ -207,7 +207,7 @@ class EvaluatorsTest : AbstractTest() {
prop1 = "value1"
prop2 = "value2"
other = null
"""
"""
.trimIndent(),
)
}
@@ -277,7 +277,7 @@ class EvaluatorsTest : AbstractTest() {
name = "Pigeon"
age = 30
}
"""
"""
.trimIndent(),
)
}
@@ -363,7 +363,7 @@ class EvaluatorsTest : AbstractTest() {
foo = 1
// hello
bar = 2
"""
"""
.trimIndent(),
)
}
@@ -378,7 +378,7 @@ class EvaluatorsTest : AbstractTest() {
output {
text = reflect.Module(module).uri
}
"""
"""
.trimIndent(),
)
@@ -395,22 +395,22 @@ class EvaluatorsTest : AbstractTest() {
"pcf",
"""
multipleFileOutputDir = layout.projectDirectory.dir("my-output")
"""
"""
.trimIndent(),
)
writeFile(
"test.pkl",
"""
output {
files {
["output-1.txt"] {
text = "My output 1"
}
["output-2.txt"] {
text = "My output 2"
}
output {
files {
["output-1.txt"] {
text = "My output 1"
}
["output-2.txt"] {
text = "My output 2"
}
}
}
"""
.trimIndent(),
)
@@ -426,15 +426,15 @@ class EvaluatorsTest : AbstractTest() {
"""
expression = "metadata.name"
outputFile = layout.projectDirectory.file("output.txt")
"""
"""
.trimIndent(),
)
writeFile(
"test.pkl",
"""
metadata {
name = "Uni"
}
metadata {
name = "Uni"
}
"""
.trimIndent(),
)
@@ -454,9 +454,9 @@ class EvaluatorsTest : AbstractTest() {
writeFile(
"test.pkl",
"""
import "package://localhost:0/birds@0.5.0#/Bird.pkl"
res = new Bird { name = "Wally"; favoriteFruit { name = "bananas" } }
import "package://localhost:0/birds@0.5.0#/Bird.pkl"
res = new Bird { name = "Wally"; favoriteFruit { name = "bananas" } }
"""
.trimIndent(),
)
@@ -472,18 +472,18 @@ class EvaluatorsTest : AbstractTest() {
"proj1/PklProject",
"""
amends "pkl:Project"
dependencies {
["proj2"] = import("../proj2/PklProject")
}
package {
name = "proj1"
baseUri = "package://localhost:0/\(name)"
version = "1.0.0"
packageZipUrl = "https://localhost:0/\(name)@\(version).zip"
}
"""
"""
.trimIndent(),
)
@@ -491,14 +491,14 @@ class EvaluatorsTest : AbstractTest() {
"proj2/PklProject",
"""
amends "pkl:Project"
package {
name = "proj2"
baseUri = "package://localhost:0/\(name)"
version = "1.0.0"
packageZipUrl = "https://localhost:0/\(name)@\(version).zip"
}
"""
"""
.trimIndent(),
)
@@ -515,7 +515,7 @@ class EvaluatorsTest : AbstractTest() {
}
}
}
"""
"""
.trimIndent(),
)
@@ -526,7 +526,7 @@ class EvaluatorsTest : AbstractTest() {
"schemaVersion": 1,
"resolvedDependencies": {}
}
"""
"""
.trimIndent(),
)
@@ -534,9 +534,9 @@ class EvaluatorsTest : AbstractTest() {
"proj1/foo.pkl",
"""
module proj1.foo
bar: String = import("@proj2/baz.pkl").qux
"""
"""
.trimIndent(),
)
@@ -544,7 +544,7 @@ class EvaluatorsTest : AbstractTest() {
"proj2/baz.pkl",
"""
qux: String = "Contents of @proj2/qux"
"""
"""
.trimIndent(),
)
@@ -595,7 +595,7 @@ class EvaluatorsTest : AbstractTest() {
}
}
}
"""
"""
.trimIndent(),
)
@@ -607,15 +607,15 @@ class EvaluatorsTest : AbstractTest() {
assertThat(result1.output)
.containsIgnoringNewLines(
"""
evalCounter.txt
doEval executed
file1.yaml
foo: 1
evalCounter.txt
doEval executed
file2.yaml
bar: 1
"""
file1.yaml
foo: 1
file2.yaml
bar: 1
"""
.trimIndent()
)
@@ -628,15 +628,15 @@ class EvaluatorsTest : AbstractTest() {
assertThat(result2.output)
.containsIgnoringNewLines(
"""
evalCounter.txt
doEval executed
file1.yaml
foo: 1
evalCounter.txt
doEval executed
file2.yaml
bar: 1
"""
file1.yaml
foo: 1
file2.yaml
bar: 1
"""
.trimIndent()
)
@@ -652,16 +652,16 @@ class EvaluatorsTest : AbstractTest() {
assertThat(result3.output)
.containsIgnoringNewLines(
"""
evalCounter.txt
doEval executed
doEval executed
file1.yaml
foo: 7
evalCounter.txt
doEval executed
doEval executed
file2.yaml
bar: 1
"""
file1.yaml
foo: 7
file2.yaml
bar: 1
"""
.trimIndent()
)
}
@@ -674,12 +674,12 @@ class EvaluatorsTest : AbstractTest() {
name = "Pigeon"
diet = "Seeds"
}
parrot {
name = "Parrot"
diet = "Seeds"
}
output {
files {
["birds/pigeon.json"] {
@@ -692,7 +692,7 @@ class EvaluatorsTest : AbstractTest() {
}
}
}
"""
"""
.trimIndent()
)
@@ -701,7 +701,7 @@ class EvaluatorsTest : AbstractTest() {
additionalContents =
"""
multipleFileOutputDir = layout.projectDirectory.dir("%{moduleDir}/%{moduleName}-%{outputFormat}")
"""
"""
.trimIndent(),
additionalBuildScript =
"""
@@ -710,12 +710,12 @@ class EvaluatorsTest : AbstractTest() {
file("evalCounter.txt").append("evalTest executed\n")
}
}
abstract class PrintTask extends DefaultTask {
@InputFiles
public abstract ConfigurableFileCollection getInputDirs();
}
// ensure that iteration order is the same across environments
def sortByTypeThenName = { a, b ->
a.isFile() != b.isFile() ? a.isFile() <=> b.isFile() : a.name <=> b.name
@@ -741,7 +741,7 @@ class EvaluatorsTest : AbstractTest() {
}
}
}
"""
"""
.trimIndent(),
)
@@ -757,23 +757,23 @@ class EvaluatorsTest : AbstractTest() {
assertThat(result1.output)
.containsIgnoringNewLines(
"""
evalCounter.txt
evalTest executed
test-yaml/birds/
test-yaml/birds/parrot.pcf
name = "Parrot"
diet = "Seeds"
test-yaml/birds/pigeon.json
{
"name": "Pigeon",
"diet": "Seeds"
}
test-yaml/
"""
evalCounter.txt
evalTest executed
test-yaml/birds/
test-yaml/birds/parrot.pcf
name = "Parrot"
diet = "Seeds"
test-yaml/birds/pigeon.json
{
"name": "Pigeon",
"diet": "Seeds"
}
test-yaml/
"""
.trimIndent()
)
@@ -786,23 +786,23 @@ class EvaluatorsTest : AbstractTest() {
assertThat(result2.output)
.containsIgnoringNewLines(
"""
evalCounter.txt
evalTest executed
test-yaml/birds/
test-yaml/birds/parrot.pcf
name = "Parrot"
diet = "Seeds"
test-yaml/birds/pigeon.json
{
"name": "Pigeon",
"diet": "Seeds"
}
test-yaml/
"""
evalCounter.txt
evalTest executed
test-yaml/birds/
test-yaml/birds/parrot.pcf
name = "Parrot"
diet = "Seeds"
test-yaml/birds/pigeon.json
{
"name": "Pigeon",
"diet": "Seeds"
}
test-yaml/
"""
.trimIndent()
)
@@ -818,24 +818,24 @@ class EvaluatorsTest : AbstractTest() {
assertThat(result3.output)
.containsIgnoringNewLines(
"""
evalCounter.txt
evalTest executed
evalTest executed
test-yaml/birds/
test-yaml/birds/parrot.pcf
name = "Macaw"
diet = "Seeds"
test-yaml/birds/pigeon.json
{
"name": "Pigeon",
"diet": "Seeds"
}
test-yaml/
"""
evalCounter.txt
evalTest executed
evalTest executed
test-yaml/birds/
test-yaml/birds/parrot.pcf
name = "Macaw"
diet = "Seeds"
test-yaml/birds/pigeon.json
{
"name": "Pigeon",
"diet": "Seeds"
}
test-yaml/
"""
.trimIndent()
)
}
@@ -874,8 +874,8 @@ class EvaluatorsTest : AbstractTest() {
"json",
additionalContents =
"""
transitiveModules.from(files("shared2.pkl"))
"""
transitiveModules.from(files("shared2.pkl"))
"""
.trimIndent(),
)
val result1 = runTask("evalTest")
@@ -896,6 +896,17 @@ class EvaluatorsTest : AbstractTest() {
assertThat(result5.task(":evalTestGatherImports")).isNull()
}
@Test
fun `is configuration cache compatible`() {
writeBuildFile("yaml")
writePklFile()
val (firstRun, secondRun) = runTaskWithConfigurationCache("evalTest")
assertThat(firstRun.output).contains(CONFIG_CACHE_STORED)
assertThat(secondRun.output).contains(CONFIG_CACHE_REUSED)
}
private fun writeBuildFile(
// don't use `org.pkl.core.OutputFormat`
// because test compile class path doesn't contain pkl-core

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -84,6 +84,28 @@ class JavaCodeGeneratorsTest : AbstractTest() {
assertThat(addressClassFile).exists()
}
@Test
fun `is configuration cache compatible`() {
writeBuildFile()
writePklFile()
val (firstRun, secondRun) = runTaskWithConfigurationCache("configClasses")
assertThat(firstRun.output).contains(CONFIG_CACHE_STORED)
assertThat(secondRun.output).contains(CONFIG_CACHE_REUSED)
val generatedModuleFile = testProjectDir.resolve("build/generated/java/foo/bar/Mod.java")
assertThat(generatedModuleFile).exists()
checkTextContains(
generatedModuleFile.readText(),
"package foo.bar;",
"public final class Mod {",
"public final @Nonnull Object other;",
"public static final class Person {",
"public final @Nonnull String name;",
)
}
@Test
fun `no source modules`() {
writeFile(
@@ -107,6 +129,53 @@ class JavaCodeGeneratorsTest : AbstractTest() {
assertThat(result.output).contains("No source modules specified.")
}
@Test
fun `source set configured after pkl block`() {
writeFile(
"build.gradle",
"""
plugins {
id "java"
id "org.pkl-lang"
}
sourceSets {
integTest {}
}
repositories {
mavenCentral()
}
dependencies {
integTestImplementation "javax.inject:javax.inject:1"
integTestImplementation "com.google.code.findbugs:jsr305:3.0.2"
}
pkl {
javaCodeGenerators {
configClasses {
sourceModules = ["mod.pkl"]
outputDir = file("build/generated")
paramsAnnotation = "javax.inject.Named"
nonNullAnnotation = "javax.annotation.Nonnull"
settingsModule = "pkl:settings"
renames = ['org': 'foo.bar']
}
}
}
pkl.javaCodeGenerators.configClasses.sourceSet = sourceSets.integTest
""",
)
writePklFile()
runTask("compileIntegTestJava")
val classesDir = testProjectDir.resolve("build/classes/java/integTest")
assertThat(classesDir.resolve("foo/bar/Mod.class")).exists()
}
private fun writeBuildFile() {
writeFile(
"build.gradle",

View File

@@ -84,6 +84,17 @@ class KotlinCodeGeneratorsTest : AbstractTest() {
assertThat(addressClassFile).exists()
}
@Test
fun `is configuration cache compatible`() {
writeBuildFile()
writePklFile()
val (firstRun, secondRun) = runTaskWithConfigurationCache("configClasses")
assertThat(firstRun.output).contains(CONFIG_CACHE_STORED)
assertThat(secondRun.output).contains(CONFIG_CACHE_REUSED)
}
@Test
fun `no source modules`() {
writeFile(

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -56,7 +56,7 @@ class PkldocGeneratorsTest : AbstractTest() {
authors { "publisher@apple.com" }
sourceCode = "sources.apple.com/"
issueTracker = "issues.apple.com"
"""
"""
.trimIndent(),
)
writeFile(
@@ -75,7 +75,7 @@ class PkldocGeneratorsTest : AbstractTest() {
}
other = 42
"""
"""
.trimIndent(),
)
@@ -134,6 +134,54 @@ class PkldocGeneratorsTest : AbstractTest() {
assertThat(birdsPackageFile).exists()
}
@Test
fun `is configuration cache compatible`() {
writeFile(
"build.gradle",
"""
plugins {
id "org.pkl-lang"
}
pkl {
pkldocGenerators {
pkldoc {
sourceModules = ["doc-package-info.pkl", "mod.pkl"]
outputDir = file("build/pkldoc")
settingsModule = "pkl:settings"
}
}
}
""",
)
writeFile(
"doc-package-info.pkl",
"""
/// Overview documentation for the test package.
amends "pkl:DocPackageInfo"
name = "testpkg"
version = "1.0.0"
importUri = "https://example.com/"
authors { "test@example.com" }
sourceCode = "https://example.com/source"
issueTracker = "https://example.com/issues"
""",
)
writeFile(
"mod.pkl",
"""
module testpkg.mod
greeting: String = "hello"
""",
)
val (firstRun, secondRun) = runTaskWithConfigurationCache("pkldoc")
assertThat(firstRun.output).contains(CONFIG_CACHE_STORED)
assertThat(secondRun.output).contains(CONFIG_CACHE_REUSED)
}
@Test
fun `no source modules`() {
writeFile(
@@ -149,7 +197,7 @@ class PkldocGeneratorsTest : AbstractTest() {
}
}
}
"""
"""
.trimIndent(),
)

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,7 +48,7 @@ class ProjectPackageTest : AbstractTest() {
"""
junitReportsDir.set(file("test-reports"))
skipPublishCheck.set(true)
"""
"""
.trimIndent()
)
writeProjectContent()
@@ -56,6 +56,17 @@ class ProjectPackageTest : AbstractTest() {
assertThat(testProjectDir.resolve("test-reports")).isNotEmptyDirectory()
}
@Test
fun `is configuration cache compatible`() {
writeBuildFile("skipPublishCheck.set(true)")
writeProjectContent()
val (firstRun, secondRun) = runTaskWithConfigurationCache("createMyPackages")
assertThat(firstRun.output).contains(CONFIG_CACHE_STORED)
assertThat(secondRun.output).contains(CONFIG_CACHE_REUSED)
}
private fun writeBuildFile(additionalContents: String = "") {
writeFile(
"build.gradle",
@@ -84,7 +95,7 @@ class ProjectPackageTest : AbstractTest() {
"proj1/PklProject",
"""
amends "pkl:Project"
package {
name = "proj1"
baseUri = "package://localhost:0/proj1"
@@ -94,7 +105,7 @@ class ProjectPackageTest : AbstractTest() {
"tests.pkl"
}
}
"""
"""
.trimIndent(),
)
writeFile(
@@ -104,29 +115,29 @@ class ProjectPackageTest : AbstractTest() {
"schemaVersion": 1,
"dependencies": {}
}
"""
"""
.trimIndent(),
)
writeFile(
"proj1/foo.pkl",
"""
module proj1.foo
bar: String
"""
"""
.trimIndent(),
)
writeFile(
"proj1/tests.pkl",
"""
amends "pkl:test"
facts {
["it works"] {
1 == 1
}
}
"""
"""
.trimIndent(),
)
writeFile("foo.txt", "The contents of foo.txt")

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,15 +27,26 @@ class ProjectResolveTest : AbstractTest() {
assertThat(testProjectDir.resolve("proj1/PklProject.deps.json"))
.hasContent(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {}
}
"""
{
"schemaVersion": 1,
"resolvedDependencies": {}
}
"""
.trimIndent()
)
}
@Test
fun `is configuration cache compatible`() {
writeBuildFile()
writeProjectContent()
val (firstRun, secondRun) = runTaskWithConfigurationCache("resolveMyProj")
assertThat(firstRun.output).contains(CONFIG_CACHE_STORED)
assertThat(secondRun.output).contains(CONFIG_CACHE_REUSED)
}
private fun writeBuildFile(additionalContents: String = "") {
writeFile(
"build.gradle",
@@ -64,7 +75,7 @@ class ProjectResolveTest : AbstractTest() {
"proj1/PklProject",
"""
amends "pkl:Project"
"""
"""
.trimIndent(),
)
}

View File

@@ -40,11 +40,11 @@ class TestsTest : AbstractTest() {
writePklFile(
additionalFacts =
"""
["should fail"] {
1 == 3
"foo" == "bar"
}
"""
["should fail"] {
1 == 3
"foo" == "bar"
}
"""
.trimIndent()
)
@@ -61,10 +61,10 @@ class TestsTest : AbstractTest() {
writePklFile(
additionalFacts =
"""
["error"] {
throw("exception")
}
"""
["error"] {
throw("exception")
}
"""
.trimIndent()
)
@@ -78,20 +78,20 @@ class TestsTest : AbstractTest() {
assertThat(output)
.containsIgnoringNewLines(
"""
> Task :evalTest FAILED
module test
facts
✔ should pass
✘ error
Pkl Error
exception
> Task :evalTest FAILED
module test
facts
✔ should pass
✘ error
Pkl Error
exception
9 | throw("exception")
^^^^^^^^^^^^^^^^^^
at test#facts["error"][#1] (file:///file, line x)
9 | throw("exception")
^^^^^^^^^^^^^^^^^^
at test#facts["error"][#1] (file:///file, line x)
50.0% tests pass [1/2 failed], 66.6% asserts pass [1/3 failed]
"""
50.0% tests pass [1/2 failed], 66.6% asserts pass [1/3 failed]
"""
.trimIndent()
)
}
@@ -169,10 +169,10 @@ class TestsTest : AbstractTest() {
writePklFile(
additionalFacts =
"""
["error"] {
throw("exception")
}
"""
["error"] {
throw("exception")
}
"""
.trimIndent(),
additionalExamples = examples,
)
@@ -189,7 +189,7 @@ class TestsTest : AbstractTest() {
.startsWith(
"""
> Task :evalTestGatherImports
> Task :evalTest FAILED
module test
facts
@@ -197,7 +197,7 @@ class TestsTest : AbstractTest() {
✘ error
Pkl Error
exception
9 | throw("exception")
^^^^^^^^^^^^^^^^^^
at test#facts["error"][#1] (file:///file, line x)
@@ -280,10 +280,10 @@ class TestsTest : AbstractTest() {
writePklFile(
additionalFacts =
"""
["error"] {
throw("exception")
}
"""
["error"] {
throw("exception")
}
"""
.trimIndent(),
additionalExamples = examples,
)
@@ -305,7 +305,7 @@ class TestsTest : AbstractTest() {
<testcase classname="test.facts" name="error">
<error message="exception"> Pkl Error
exception
9 | throw(&quot;exception&quot;)
^^^^^^^^^^^^^^^^^^
at test#facts[&quot;error&quot;][#1] (file:///file, line x)
@@ -334,23 +334,23 @@ class TestsTest : AbstractTest() {
private val examples =
"""
["user 0"] {
new {
name = "Cool"
age = 11
}
["user 0"] {
new {
name = "Cool"
age = 11
}
["user 1"] {
new {
name = "Pigeon"
age = 40
}
new {
name = "Welma"
age = 35
}
}
["user 1"] {
new {
name = "Pigeon"
age = 40
}
"""
new {
name = "Welma"
age = 35
}
}
"""
.trimIndent()
private val bigTest =
@@ -400,9 +400,20 @@ class TestsTest : AbstractTest() {
}
}
}
"""
"""
.trimIndent()
@Test
fun `is configuration cache compatible`() {
writeBuildFile()
writePklFile()
val (firstRun, secondRun) = runTaskWithConfigurationCache("evalTest")
assertThat(firstRun.output).contains(CONFIG_CACHE_STORED)
assertThat(secondRun.output).contains(CONFIG_CACHE_REUSED)
}
private fun writeBuildFile(additionalContents: String = "") {
writeFile(
"build.gradle",