From d4dacd5a0f9fc17428fb01dce890c895a10f39f5 Mon Sep 17 00:00:00 2001 From: Lucas Shadler <99682335+ffluk3@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:02:13 -0700 Subject: [PATCH] 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. --- .../main/java/org/pkl/gradle/PklPlugin.java | 265 +++++++++-------- .../pkl/gradle/task/AnalyzeImportsTask.java | 18 +- .../java/org/pkl/gradle/task/BasePklTask.java | 78 ++--- .../java/org/pkl/gradle/task/EvalTask.java | 32 +-- .../org/pkl/gradle/task/JavaCodeGenTask.java | 2 +- .../pkl/gradle/task/KotlinCodeGenTask.java | 2 +- .../java/org/pkl/gradle/task/ModulesTask.java | 68 ++--- .../org/pkl/gradle/utils/PluginUtils.java | 37 ++- .../kotlin/org/pkl/gradle/AbstractTest.kt | 61 +++- .../kotlin/org/pkl/gradle/EvaluatorsTest.kt | 269 +++++++++--------- .../org/pkl/gradle/JavaCodeGeneratorsTest.kt | 71 ++++- .../pkl/gradle/KotlinCodeGeneratorsTest.kt | 11 + .../org/pkl/gradle/PkldocGeneratorsTest.kt | 56 +++- .../org/pkl/gradle/ProjectPackageTest.kt | 29 +- .../org/pkl/gradle/ProjectResolveTest.kt | 25 +- .../test/kotlin/org/pkl/gradle/TestsTest.kt | 107 +++---- 16 files changed, 695 insertions(+), 436 deletions(-) diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java b/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java index 943bf2f3..28805905 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java @@ -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 { 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 specs) { + private void configureProjectPackageTasks( + Project project, NamedDomainObjectContainer 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 { 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 { }); } - private void configureProjectResolveTasks(NamedDomainObjectContainer specs) { + private void configureProjectResolveTasks( + Project project, NamedDomainObjectContainer 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 specs) { + private void configureAnalyzeImportsTasks( + Project project, NamedDomainObjectContainer 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 specs) { + private void configureEvalTasks(Project project, NamedDomainObjectContainer 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 { 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 { }); } - private void configureJavaCodeGenTasks(NamedDomainObjectContainer specs) { + private void configureJavaCodeGenTasks( + Project project, NamedDomainObjectContainer 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 { // 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() - .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 specs) { + private void configureKotlinCodeGenTasks( + Project project, NamedDomainObjectContainer 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() - .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 specs) { + private void configurePkldocTasks(Project project, NamedDomainObjectContainer 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 { }); } - private void configureTestTasks(NamedDomainObjectContainer specs) { + private void configureTestTasks(Project project, NamedDomainObjectContainer 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 { }); } - 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 { 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 { 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 { 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 { // 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 { .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> 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 { 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 { task.getRenames().set(spec.getRenames()); } - private void configureBaseTask(T task, S spec) { + private 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 { task.getHttpRewrites().set(spec.getHttpRewrites()); } - private List 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 void configureModulesTask( + Project project, T task, S spec, @Nullable TaskProvider analyzeImportsTask, @Nullable Transformer, 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 { 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 void configureModulesTask( - T task, S spec, @Nullable TaskProvider analyzeImportsTask) { - configureModulesTask(task, spec, analyzeImportsTask, null); + Project project, + T task, + S spec, + @Nullable TaskProvider analyzeImportsTask) { + configureModulesTask(project, task, spec, analyzeImportsTask, null); } - private TaskProvider createAnalyzeImportsTask(ModulesSpec spec) { + private TaskProvider 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 { * needing to manually provide transitive modules. */ private TaskProvider createModulesTask( - Class taskClass, ModulesSpec spec) { - var analyzeImportsTask = createAnalyzeImportsTask(spec); - return project() + Project project, Class 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 TaskProvider createTask(Class taskClass, BasePklSpec spec) { - return project() + private TaskProvider createTask( + Project project, Class taskClass, BasePklSpec spec) { + return project .getTasks() - .register(spec.getName(), taskClass, task -> configureBaseTask(task, spec)); + .register(spec.getName(), taskClass, task -> configureBaseTask(project, task, spec)); } private Set append(Set set1, T element) { diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/AnalyzeImportsTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/AnalyzeImportsTask.java index ea0cec9c..5c3d93b0 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/AnalyzeImportsTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/AnalyzeImportsTask.java @@ -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 getOutputFormat(); - private final Provider 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(); } } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java index a8d5fda0..bfd3a4b6 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java @@ -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 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 diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java index 42184c6e..804f38e4 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java @@ -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 getExpression(); - private final Provider 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 Set nullToEmpty(@Nullable Set 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(); } } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/JavaCodeGenTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/JavaCodeGenTask.java index ec69ec7a..7b4c25d0 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/JavaCodeGenTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/JavaCodeGenTask.java @@ -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(), diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java index 83e2b5cb..2f1b8c7f 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java @@ -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(), diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java index 93bc23b7..4f6a0347 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java @@ -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 getTransitiveModules(); - private final Map, Pair, List>> parsedSourceModulesCache = - new HashMap<>(); - // Used for input tracking purposes only. @Internal public Provider, List>> 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)); } } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/utils/PluginUtils.java b/pkl-gradle/src/main/java/org/pkl/gradle/utils/PluginUtils.java index 2b7f285f..43e5e103 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/utils/PluginUtils.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/utils/PluginUtils.java @@ -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. + * + *

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 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); + } + } } diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/AbstractTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/AbstractTest.kt index 90f81e90..852dabed 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/AbstractTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/AbstractTest.kt @@ -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 { + 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() diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt index a8b778a3..6db78751 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt @@ -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() { - """ + """ .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 diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/JavaCodeGeneratorsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/JavaCodeGeneratorsTest.kt index 68a5b533..31c856f0 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/JavaCodeGeneratorsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/JavaCodeGeneratorsTest.kt @@ -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", diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/KotlinCodeGeneratorsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/KotlinCodeGeneratorsTest.kt index 5438361a..6bfa891f 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/KotlinCodeGeneratorsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/KotlinCodeGeneratorsTest.kt @@ -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( diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/PkldocGeneratorsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/PkldocGeneratorsTest.kt index e476ec67..d48186b8 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/PkldocGeneratorsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/PkldocGeneratorsTest.kt @@ -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(), ) diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/ProjectPackageTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/ProjectPackageTest.kt index 29ca4fd0..6e51f5d7 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/ProjectPackageTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/ProjectPackageTest.kt @@ -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") diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/ProjectResolveTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/ProjectResolveTest.kt index 5013e94f..409d4875 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/ProjectResolveTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/ProjectResolveTest.kt @@ -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(), ) } diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt index 1594e1b3..50538a1f 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt @@ -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() { –– Pkl Error –– exception - + 9 | throw("exception") ^^^^^^^^^^^^^^^^^^ at test#facts["error"][#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",