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",