mirror of
https://github.com/apple/pkl.git
synced 2026-01-11 22:30:54 +01:00
Make EvalTask track resolved output paths and fix file() notation on Gradle on Windows (#403)
* Add `getEffectiveOutputFiles` and `getEffectiveOutputDirs` to `EvalTask`, and mark them as output files/dirs so they are tracked by Gradle. This enables implicit dependency tracking between two tasks. * Fix usage of `file()` notation in Gradle scripts when on Windows.
This commit is contained in:
@@ -51,9 +51,10 @@ constructor(
|
||||
private val consoleWriter: Writer = System.out.writer(),
|
||||
) : CliCommand(options.base) {
|
||||
/**
|
||||
* Output files for the modules to be evaluated. Returns `null` if `options.outputPath` is `null`.
|
||||
* Multiple modules may be mapped to the same output file, in which case their outputs are
|
||||
* concatenated with [CliEvaluatorOptions.moduleOutputSeparator].
|
||||
* Output files for the modules to be evaluated. Returns `null` if `options.outputPath` is `null`
|
||||
* or if `options.multipleFileOutputPath` is not `null`. Multiple modules may be mapped to the
|
||||
* same output file, in which case their outputs are concatenated with
|
||||
* [CliEvaluatorOptions.moduleOutputSeparator].
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
val outputFiles: Set<File>? by lazy {
|
||||
|
||||
@@ -28,15 +28,18 @@ import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.inject.Inject;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.InvalidUserDataException;
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.FileSystemLocation;
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.MapProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.provider.ProviderFactory;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.InputFiles;
|
||||
@@ -142,6 +145,7 @@ public abstract class BasePklTask extends DefaultTask {
|
||||
|
||||
@LateInit protected CliBaseOptions cachedOptions;
|
||||
|
||||
// Must be called during task execution time only.
|
||||
@Internal
|
||||
protected CliBaseOptions getCliBaseOptions() {
|
||||
if (cachedOptions == null) {
|
||||
@@ -176,6 +180,12 @@ public abstract class BasePklTask extends DefaultTask {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Inject
|
||||
protected abstract ObjectFactory getObjects();
|
||||
|
||||
@Inject
|
||||
protected abstract ProviderFactory getProviders();
|
||||
|
||||
protected List<Path> parseModulePath() {
|
||||
return getModulePath().getFiles().stream().map(File::toPath).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -16,50 +16,82 @@
|
||||
package org.pkl.gradle.task;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
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.Input;
|
||||
import org.gradle.api.tasks.Internal;
|
||||
import org.gradle.api.tasks.Optional;
|
||||
import org.gradle.api.tasks.OutputDirectory;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.OutputDirectories;
|
||||
import org.gradle.api.tasks.OutputFiles;
|
||||
import org.pkl.cli.CliEvaluator;
|
||||
import org.pkl.cli.CliEvaluatorOptions;
|
||||
|
||||
public abstract class EvalTask extends ModulesTask {
|
||||
@OutputFile
|
||||
@Optional
|
||||
|
||||
// not tracked because it might contain placeholders
|
||||
// required
|
||||
@Internal
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
public abstract Property<String> getOutputFormat();
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
public abstract Property<String> getModuleOutputSeparator();
|
||||
|
||||
@OutputDirectory
|
||||
@Optional
|
||||
// not tracked because it might contain placeholders
|
||||
// optional
|
||||
@Internal
|
||||
public abstract DirectoryProperty getMultipleFileOutputDir();
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
public abstract Property<String> getOutputFormat();
|
||||
|
||||
@Input
|
||||
public abstract Property<String> getModuleOutputSeparator();
|
||||
|
||||
@Input
|
||||
public abstract Property<String> getExpression();
|
||||
|
||||
private final Provider<CliEvaluator> cliEvaluator =
|
||||
getProviders()
|
||||
.provider(
|
||||
() ->
|
||||
new CliEvaluator(
|
||||
new CliEvaluatorOptions(
|
||||
getCliBaseOptions(),
|
||||
getOutputFile().get().getAsFile().getAbsolutePath(),
|
||||
getOutputFormat().get(),
|
||||
getModuleOutputSeparator().get(),
|
||||
mapAndGetOrNull(
|
||||
getMultipleFileOutputDir(), it -> it.getAsFile().getAbsolutePath()),
|
||||
getExpression().get())));
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@OutputFiles
|
||||
@Optional
|
||||
public FileCollection getEffectiveOutputFiles() {
|
||||
return getObjects()
|
||||
.fileCollection()
|
||||
.from(cliEvaluator.map(e -> nullToEmpty(e.getOutputFiles())));
|
||||
}
|
||||
|
||||
@OutputDirectories
|
||||
@Optional
|
||||
public FileCollection getEffectiveOutputDirs() {
|
||||
return getObjects()
|
||||
.fileCollection()
|
||||
.from(cliEvaluator.map(e -> nullToEmpty(e.getOutputDirectories())));
|
||||
}
|
||||
|
||||
private static <T> Set<T> nullToEmpty(@Nullable Set<T> set) {
|
||||
return set == null ? Collections.emptySet() : set;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doRunTask() {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
getOutputs().getPreviousOutputFiles().forEach(File::delete);
|
||||
|
||||
new CliEvaluator(
|
||||
new CliEvaluatorOptions(
|
||||
getCliBaseOptions(),
|
||||
getOutputFile().get().getAsFile().getAbsolutePath(),
|
||||
getOutputFormat().get(),
|
||||
getModuleOutputSeparator().get(),
|
||||
mapAndGetOrNull(getMultipleFileOutputDir(), it -> it.getAsFile().getAbsolutePath()),
|
||||
getExpression().get()))
|
||||
.run();
|
||||
cliEvaluator.get().run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +128,15 @@ public abstract class ModulesTask extends BasePklTask {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts either a file or a URI to a URI. We convert a file to a URI via the {@link
|
||||
* Converts either a file or a URI to a URI. We convert a relative file to a URI via the {@link
|
||||
* IoUtils#createUri(String)} because other ways of conversion can make relative paths into
|
||||
* absolute URIs, which may break module loading.
|
||||
*/
|
||||
private URI parsedModuleNotationToUri(Object notation) {
|
||||
if (notation instanceof File file) {
|
||||
if (file.isAbsolute()) {
|
||||
return file.toPath().toUri();
|
||||
}
|
||||
return IoUtils.createUri(IoUtils.toNormalizedPathString(file.toPath()));
|
||||
} else if (notation instanceof URI uri) {
|
||||
return uri;
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
package org.pkl.gradle
|
||||
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.readText
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gradle.testkit.runner.TaskOutcome
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.readString
|
||||
@@ -549,12 +551,301 @@ class EvaluatorsTest : AbstractTest() {
|
||||
assertThat(testProjectDir.resolve("proj1/foo.pcf")).exists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `implicit dependency tracking for effective output files`() {
|
||||
writeFile("file1.pkl", "foo = 1")
|
||||
|
||||
writeFile("file2.pkl", "bar = 1")
|
||||
|
||||
writeFile(
|
||||
"build.gradle.kts",
|
||||
"""
|
||||
import org.pkl.gradle.task.EvalTask
|
||||
|
||||
plugins {
|
||||
id("org.pkl-lang")
|
||||
}
|
||||
|
||||
pkl {
|
||||
evaluators {
|
||||
register("doEval") {
|
||||
sourceModules = files("file1.pkl", "file2.pkl")
|
||||
outputFile = layout.projectDirectory.file("%{moduleName}.%{outputFormat}")
|
||||
outputFormat = "yaml"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val doEval by tasks.existing(EvalTask::class) {
|
||||
doLast {
|
||||
file("evalCounter.txt").appendText("doEval executed\n")
|
||||
}
|
||||
}
|
||||
|
||||
val printEvalFiles by tasks.registering {
|
||||
inputs.files(doEval)
|
||||
doLast {
|
||||
println("evalCounter.txt")
|
||||
println(file("evalCounter.txt").readText())
|
||||
|
||||
inputs.files.forEach {
|
||||
println(it.name)
|
||||
println(it.readText())
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
val result1 = runTask("printEvalFiles")
|
||||
|
||||
// `doEval` task is invoked transitively.
|
||||
assertThat(result1.task(":doEval")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
|
||||
|
||||
assertThat(result1.output)
|
||||
.containsIgnoringNewLines(
|
||||
"""
|
||||
evalCounter.txt
|
||||
doEval executed
|
||||
|
||||
file1.yaml
|
||||
foo: 1
|
||||
|
||||
file2.yaml
|
||||
bar: 1
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
// Run the task again to check that it is cached.
|
||||
val result2 = runTask("printEvalFiles")
|
||||
|
||||
assertThat(result2.task(":doEval")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
|
||||
|
||||
// evalCounter.txt content is the same as before.
|
||||
assertThat(result2.output)
|
||||
.containsIgnoringNewLines(
|
||||
"""
|
||||
evalCounter.txt
|
||||
doEval executed
|
||||
|
||||
file1.yaml
|
||||
foo: 1
|
||||
|
||||
file2.yaml
|
||||
bar: 1
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
// Modify the input file.
|
||||
writeFile("file1.pkl", "foo = 7")
|
||||
|
||||
// Run the build again - the evaluation task will not be cached.
|
||||
val result3 = runTask("printEvalFiles")
|
||||
|
||||
assertThat(result3.task(":doEval")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
|
||||
|
||||
// evalCounter.txt content is updated too.
|
||||
assertThat(result3.output)
|
||||
.containsIgnoringNewLines(
|
||||
"""
|
||||
evalCounter.txt
|
||||
doEval executed
|
||||
doEval executed
|
||||
|
||||
file1.yaml
|
||||
foo: 7
|
||||
|
||||
file2.yaml
|
||||
bar: 1
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `implicit dependency tracking for multiple output directory`() {
|
||||
writePklFile(
|
||||
"""
|
||||
pigeon {
|
||||
name = "Pigeon"
|
||||
diet = "Seeds"
|
||||
}
|
||||
|
||||
parrot {
|
||||
name = "Parrot"
|
||||
diet = "Seeds"
|
||||
}
|
||||
|
||||
output {
|
||||
files {
|
||||
["birds/pigeon.json"] {
|
||||
value = pigeon
|
||||
renderer = new JsonRenderer {}
|
||||
}
|
||||
["birds/parrot.pcf"] {
|
||||
value = parrot
|
||||
renderer = new PcfRenderer {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
writeBuildFile(
|
||||
"yaml",
|
||||
additionalContents =
|
||||
"""
|
||||
multipleFileOutputDir = layout.projectDirectory.dir("%{moduleDir}/%{moduleName}-%{outputFormat}")
|
||||
"""
|
||||
.trimIndent(),
|
||||
additionalBuildScript =
|
||||
"""
|
||||
tasks.named('evalTest') {
|
||||
doLast {
|
||||
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
|
||||
}
|
||||
|
||||
tasks.register('printEvalDirs', PrintTask) {
|
||||
inputDirs.from(tasks.named('evalTest'))
|
||||
|
||||
doLast {
|
||||
println "evalCounter.txt"
|
||||
println file("evalCounter.txt").text
|
||||
|
||||
inputDirs.forEach { f ->
|
||||
f.traverse(visitRoot: true, sort: sortByTypeThenName) {
|
||||
if (it.isDirectory()) {
|
||||
println layout.projectDirectory.asFile.relativePath(it) + '/'
|
||||
println()
|
||||
} else {
|
||||
println layout.projectDirectory.asFile.relativePath(it)
|
||||
println it.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
val result1 = runTask("printEvalDirs")
|
||||
|
||||
// `doEval` task is invoked transitively.
|
||||
assertThat(result1.task(":evalTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
|
||||
|
||||
// NB: Configured output format, 'yaml', is only used to replace the placeholder in the path;
|
||||
// the output files themselves are formatted according to configuration
|
||||
// in the rendered module.
|
||||
|
||||
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/
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
// Run the task again to check that it is cached.
|
||||
val result2 = runTask("printEvalDirs")
|
||||
|
||||
assertThat(result2.task(":evalTest")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
|
||||
|
||||
// evalCounter.txt content is the same as before.
|
||||
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/
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
// Modify the input file.
|
||||
writePklFile(testProjectDir.resolve("test.pkl").readText().replace("Parrot", "Macaw"))
|
||||
|
||||
// Run the build again - the evaluation task will not be cached.
|
||||
val result3 = runTask("printEvalDirs")
|
||||
|
||||
assertThat(result3.task(":evalTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
|
||||
|
||||
// evalCounter.txt content is updated too.
|
||||
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/
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun writeBuildFile(
|
||||
// don't use `org.pkl.core.OutputFormat`
|
||||
// because test compile class path doesn't contain pkl-core
|
||||
outputFormat: String,
|
||||
additionalContents: String = "",
|
||||
sourceModules: List<String> = listOf("test.pkl")
|
||||
sourceModules: List<String> = listOf("test.pkl"),
|
||||
additionalBuildScript: String = ""
|
||||
) {
|
||||
writeFile(
|
||||
"build.gradle",
|
||||
@@ -573,6 +864,8 @@ class EvaluatorsTest : AbstractTest() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$additionalBuildScript
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user