Initial commit

This commit is contained in:
Peter Niederwieser
2016-01-19 14:51:19 +01:00
committed by Dan Chao
commit ecad035dca
2972 changed files with 211653 additions and 0 deletions
@@ -0,0 +1,67 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle;
import org.gradle.api.Action;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.tasks.Nested;
import org.pkl.gradle.spec.EvalSpec;
import org.pkl.gradle.spec.JavaCodeGenSpec;
import org.pkl.gradle.spec.KotlinCodeGenSpec;
import org.pkl.gradle.spec.PkldocSpec;
import org.pkl.gradle.spec.TestSpec;
@SuppressWarnings("unused")
public interface PklExtension {
NamedDomainObjectContainer<EvalSpec> getEvaluators();
NamedDomainObjectContainer<JavaCodeGenSpec> getJavaCodeGenerators();
NamedDomainObjectContainer<KotlinCodeGenSpec> getKotlinCodeGenerators();
NamedDomainObjectContainer<PkldocSpec> getPkldocGenerators();
NamedDomainObjectContainer<TestSpec> getTests();
@Nested
PklProjectCommands getProject();
default void evaluators(Action<? super NamedDomainObjectContainer<EvalSpec>> action) {
action.execute(getEvaluators());
}
default void javaCodeGenerators(
Action<? super NamedDomainObjectContainer<JavaCodeGenSpec>> action) {
action.execute(getJavaCodeGenerators());
}
default void kotlinCodeGenerators(
Action<? super NamedDomainObjectContainer<KotlinCodeGenSpec>> action) {
action.execute(getKotlinCodeGenerators());
}
default void pkldocGenerators(Action<? super NamedDomainObjectContainer<PkldocSpec>> action) {
action.execute(getPkldocGenerators());
}
default void tests(Action<? super NamedDomainObjectContainer<TestSpec>> action) {
action.execute(getTests());
}
default void project(Action<? super PklProjectCommands> action) {
action.execute(getProject());
}
}
@@ -0,0 +1,519 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.gradle.api.GradleException;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.file.SourceDirectorySet;
import org.gradle.api.plugins.Convention;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.gradle.plugins.ide.idea.model.IdeaModel;
import org.gradle.util.GradleVersion;
import org.pkl.cli.CliEvaluatorOptions;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.LateInit;
import org.pkl.gradle.spec.BasePklSpec;
import org.pkl.gradle.spec.CodeGenSpec;
import org.pkl.gradle.spec.EvalSpec;
import org.pkl.gradle.spec.JavaCodeGenSpec;
import org.pkl.gradle.spec.KotlinCodeGenSpec;
import org.pkl.gradle.spec.ModulesSpec;
import org.pkl.gradle.spec.PkldocSpec;
import org.pkl.gradle.spec.ProjectPackageSpec;
import org.pkl.gradle.spec.ProjectResolveSpec;
import org.pkl.gradle.spec.TestSpec;
import org.pkl.gradle.task.BasePklTask;
import org.pkl.gradle.task.CodeGenTask;
import org.pkl.gradle.task.EvalTask;
import org.pkl.gradle.task.JavaCodeGenTask;
import org.pkl.gradle.task.KotlinCodeGenTask;
import org.pkl.gradle.task.ModulesTask;
import org.pkl.gradle.task.PkldocTask;
import org.pkl.gradle.task.ProjectPackageTask;
import org.pkl.gradle.task.ProjectResolveTask;
import org.pkl.gradle.task.TestTask;
@SuppressWarnings("unused")
public class PklPlugin implements Plugin<Project> {
private static final String MIN_GRADLE_VERSION = "7.2";
@LateInit private Project project;
@Override
public void apply(Project project) {
this.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);
}
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());
}
private void configureProjectPackageTasks(NamedDomainObjectContainer<ProjectPackageSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
spec.getOutputPath()
.convention(project.getLayout().getBuildDirectory().dir("generated/pkl/packages"));
spec.getOverwrite().convention(false);
var packageTask = createTask(ProjectPackageTask.class, spec);
packageTask.configure(
task -> {
task.getProjectDirectories().from(spec.getProjectDirectories());
task.getOutputPath().set(spec.getOutputPath());
task.getSkipPublishCheck().set(spec.getSkipPublishCheck());
task.getJunitReportsDir().set(spec.getJunitReportsDir());
task.getOverwrite().set(spec.getOverwrite());
});
project
.getPluginManager()
.withPlugin(
"base",
appliedPlugin ->
project
.getTasks()
.named(
LifecycleBasePlugin.BUILD_TASK_NAME,
it -> it.dependsOn(packageTask)));
});
}
private void configureProjectResolveTasks(NamedDomainObjectContainer<ProjectResolveSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
var resolveTask = createTask(ProjectResolveTask.class, spec);
resolveTask.configure(
task -> task.getProjectDirectories().from(spec.getProjectDirectories()));
});
}
private void configureEvalTasks(NamedDomainObjectContainer<EvalSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
spec.getOutputFile()
.convention(
project
.getLayout()
.getProjectDirectory()
// %{moduleDir} is resolved relatively to the working directory,
// and the working directory is set to the project directory,
// so this path works correctly.
.file("%{moduleDir}/%{moduleName}.%{outputFormat}"));
spec.getOutputFormat().convention("pcf");
spec.getModuleOutputSeparator()
.convention(CliEvaluatorOptions.Companion.getDefaults().getModuleOutputSeparator());
spec.getExpression()
.convention(CliEvaluatorOptions.Companion.getDefaults().getExpression());
createModulesTask(EvalTask.class, spec)
.configure(
task -> {
task.getOutputFile().set(spec.getOutputFile());
task.getOutputFormat().set(spec.getOutputFormat());
task.getModuleOutputSeparator().set(spec.getModuleOutputSeparator());
task.getMultipleFileOutputDir().set(spec.getMultipleFileOutputDir());
task.getExpression().set(spec.getExpression());
});
});
}
private void configureJavaCodeGenTasks(NamedDomainObjectContainer<JavaCodeGenSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureCodeGenSpec(spec);
spec.getGenerateGetters().convention(false);
spec.getGenerateJavadoc().convention(false);
createModulesTask(JavaCodeGenTask.class, spec)
.configure(
task -> {
configureCodeGenTask(task, spec);
task.getGenerateGetters().set(spec.getGenerateGetters());
task.getGenerateJavadoc().set(spec.getGenerateJavadoc());
task.getParamsAnnotation().set(spec.getParamsAnnotation());
task.getNonNullAnnotation().set(spec.getNonNullAnnotation());
});
});
project.afterEvaluate(
prj ->
specs.all(
spec -> {
configureIdeaModule(spec);
configureCodeGenSpecSourceDirectories(
spec, "java", s -> Optional.of(s.getJava()));
}));
}
private void configureKotlinCodeGenTasks(NamedDomainObjectContainer<KotlinCodeGenSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
configureCodeGenSpec(spec);
spec.getGenerateKdoc().convention(false);
createModulesTask(KotlinCodeGenTask.class, spec)
.configure(
task -> {
configureCodeGenTask(task, spec);
task.getGenerateKdoc().set(spec.getGenerateKdoc());
});
});
project.afterEvaluate(
prj ->
specs.all(
spec -> {
configureIdeaModule(spec);
configureCodeGenSpecSourceDirectories(
spec, "kotlin", this::getKotlinSourceDirectorySet);
}));
}
private void configurePkldocTasks(NamedDomainObjectContainer<PkldocSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
spec.getOutputDir()
.convention(
project
.getLayout()
.getBuildDirectory()
.map(it -> it.dir("pkldoc").dir(spec.getName())));
createModulesTask(PkldocTask.class, spec)
.configure(task -> task.getOutputDir().set(spec.getOutputDir()));
});
}
private void configureTestTasks(NamedDomainObjectContainer<TestSpec> specs) {
specs.all(
spec -> {
configureBaseSpec(spec);
spec.getOverwrite().convention(false);
var testTask = createModulesTask(TestTask.class, spec);
testTask.configure(
task -> {
task.getJunitReportsDir().set(spec.getJunitReportsDir());
task.getOverwrite().set(spec.getOverwrite());
});
project
.getPluginManager()
.withPlugin(
"base",
appliedPlugin ->
project
.getTasks()
.named(
LifecycleBasePlugin.CHECK_TASK_NAME,
checkTask -> checkTask.dependsOn(testTask)));
});
}
private void configureBaseSpec(BasePklSpec spec) {
spec.getAllowedModules()
.convention(
List.of(
"repl:", "file:", "modulepath:", "https:", "pkl:", "package:", "projectpackage:"));
spec.getAllowedResources()
.convention(List.of("env:", "prop:", "file:", "modulepath:", "https:", "package:"));
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
// (e.g., Test) but inconsistent with the Pkl CLI.
// Therefore, we don't set any initial value for the environmentVariables property.
// Not using `convention()` to allow the user to unset this property, disabling the cache.
spec.getModuleCacheDir().set(IoUtils.getDefaultModuleCacheDir().toFile());
spec.getNoCache().convention(false);
}
private void configureCodeGenSpec(CodeGenSpec spec) {
spec.getOutputDir()
.convention(
project
.getLayout()
.getBuildDirectory()
.map(it -> it.dir("generated").dir("pkl").dir(spec.getName())));
spec.getSourceSet()
.convention(
project
.getProviders()
.provider(
() -> {
var sourceSets = project.getExtensions().findByType(SourceSetContainer.class);
if (sourceSets == null) {
return null;
}
return sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME);
}));
spec.getIndent().convention(" ");
spec.getGenerateSpringBootConfig().convention(false);
spec.getImplementSerializable().convention(false);
configureCodeGenSpecModulePath(spec);
}
private void configureCodeGenSpecModulePath(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
// properly declares upstream libraries as `api` dependencies.
// We must not use the processResources task as an input here, because it would introduce
// a circular dependency (because codegen also generates a resources directory).
//
// Also note that in this case, we are NOT setting a dependency from the
// spec.getModulePath() file collection to the sourceSet.getResources().getSourceDirectories()
// file collection. Doing that would make spec.getModulePath() propagate a dependency
// on the tasks which contribute to sourceSet.getResources().getSourceDirectories(),
// and one of them is our own codegen task, which would result in a circular dependency.
// Refer to configureCodeGenSpecSourceDirectories for logic which links the codegen task
// to sourceSet.getResources().getSourceDirectories().
var modulePath = project.files();
modulePath
.from(getResourceSourceDirectoriesExceptSpecOutput(spec))
// This technically breaks the dependency on compile classpath builder tasks,
// however, compile classpath on a source set is always a plain configuration which
// has no builder tasks, so this is not an issue.
.from(spec.getSourceSet().map(SourceSet::getCompileClasspath));
spec.getModulePath().from(modulePath);
}
private Provider<Set<File>> getResourceSourceDirectoriesExceptSpecOutput(CodeGenSpec spec) {
// Intentionally break the dependency on source set's resources source directory set
// builder tasks by using `getFiles()` instead of `FileCollection`
// returned by `getSourceDirectories()`.
// Additionally, we must exclude our own output, to avoid creating circular dependencies
// at runtime which invalidate task execution cache.
return spec.getSourceSet()
.flatMap(
sourceSet ->
spec.getOutputDir()
.map(
specOutputDir ->
sourceSet
.getResources()
.getSourceDirectories()
.filter(
f ->
!f.getAbsolutePath()
.startsWith(
specOutputDir.getAsFile().getAbsolutePath()))
.getFiles()));
}
// Must be called from Project.afterEvaluate() only, because this method depends
// on user-provided configuration not accessible with lazy configuration.
private void configureCodeGenSpecSourceDirectories(
CodeGenSpec spec,
String languageName,
Function<? super SourceSet, ? extends Optional<SourceDirectorySet>>
extractSourceDirectorySet) {
var task = project.getTasks().named(spec.getName(), CodeGenTask.class);
var sourceSet = spec.getSourceSet().get();
extractSourceDirectorySet
.apply(sourceSet)
.ifPresentOrElse(
dirSet -> dirSet.srcDir(task.flatMap(t -> t.getOutputDir().dir(languageName))),
() ->
project
.getLogger()
.debug(
"Source directory set for language {} is not available, "
+ "will not add task {} as its dependency",
languageName,
task.getName()));
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
.getPluginManager()
.withPlugin(
"idea",
plugin -> {
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")) {
module.setTestSourceDirs(append(module.getTestSourceDirs(), outputDir));
} else {
module.setSourceDirs(append(module.getSourceDirs(), outputDir));
}
});
}
private void configureCodeGenTask(CodeGenTask task, CodeGenSpec spec) {
task.getIndent().set(spec.getIndent());
task.getOutputDir().set(spec.getOutputDir());
task.getGenerateSpringBootConfig().set(spec.getGenerateSpringBootConfig());
task.getImplementSerializable().set(spec.getImplementSerializable());
}
private <T extends BasePklTask, S extends BasePklSpec> void configureBaseTask(T task, S spec) {
task.getAllowedModules().set(spec.getAllowedModules());
task.getAllowedResources().set(spec.getAllowedResources());
task.getEnvironmentVariables().set(spec.getEnvironmentVariables());
task.getExternalProperties().set(spec.getExternalProperties());
task.getModulePath().from(spec.getModulePath());
task.getSettingsModule().set(spec.getSettingsModule());
task.getEvalRootDir().set(spec.getEvalRootDir());
task.getNoCache().set(spec.getNoCache());
task.getModuleCacheDir().set(spec.getModuleCacheDir());
task.getEvalTimeout().set(spec.getEvalTimeout());
}
private <T extends ModulesTask, S extends ModulesSpec> void configureModulesTask(T task, S spec) {
configureBaseTask(task, spec);
task.getSourceModules().set(spec.getSourceModules());
task.getTransitiveModules().from(spec.getTransitiveModules());
task.getNoProject().set(spec.getNoProject());
task.getProjectDir().set(spec.getProjectDir());
task.getOmitProjectSettings().set(spec.getOmitProjectSettings());
}
private <T extends ModulesTask> TaskProvider<T> createModulesTask(
Class<T> taskClass, ModulesSpec spec) {
return project
.getTasks()
.register(spec.getName(), taskClass, task -> configureModulesTask(task, spec));
}
private <T extends BasePklTask> TaskProvider<T> createTask(Class<T> taskClass, BasePklSpec spec) {
return project
.getTasks()
.register(spec.getName(), taskClass, task -> configureBaseTask(task, spec));
}
private <T> Set<T> append(Set<? extends T> set1, T element) {
Set<T> result = new LinkedHashSet<>(set1.size() + 1);
result.addAll(set1);
result.add(element);
return result;
}
private Optional<SourceDirectorySet> getKotlinSourceDirectorySet(SourceSet sourceSet) {
// First, try loading it as an extension - 1.8+ version of Kotlin plugin does this.
var kotlinExtension = sourceSet.getExtensions().findByName("kotlin");
if (kotlinExtension instanceof SourceDirectorySet) {
return Optional.of((SourceDirectorySet) kotlinExtension);
}
// Otherwise, try to load it as a convention. First, we attempt to get the convention
// object of the source set via the HasConvention.getConvention() method.
// We don't use the HasConvention interface directly as it is deprecated.
// Then, we extract the `kotlin` plugin from the convention, which "provides"
// the additional properties for the source set. This plugin has a method named
// `getKotlin` whose return type is a source directory set, so we use reflection
// to call it too.
// Basically, this is equivalent to calling `sourceSet.kotlin`, where `kotlin` is a property
// contributed by a plugin also named `kotlin`.
// This part of logic can be removed once we stop supporting Kotlin plugin with version
// less than 1.8.x.
try {
var getConventionMethod = sourceSet.getClass().getMethod("getConvention");
var convention = getConventionMethod.invoke(sourceSet);
if (convention instanceof Convention) {
var kotlinSourceSet = ((Convention) convention).getPlugins().get("kotlin");
if (kotlinSourceSet == null) {
project
.getLogger()
.debug(
"Cannot obtain Kotlin source directory set of source set [{}], "
+ "it does not have the `kotlin` convention plugin",
sourceSet.getName());
return Optional.empty();
}
var getKotlinMethod = kotlinSourceSet.getClass().getMethod("getKotlin");
var kotlinSourceDirectorySet = getKotlinMethod.invoke(kotlinSourceSet);
if (kotlinSourceDirectorySet instanceof SourceDirectorySet) {
return Optional.of((SourceDirectorySet) kotlinSourceDirectorySet);
}
project
.getLogger()
.debug(
"Cannot obtain Kotlin source directory set, sourceSets.{}.kotlin is of wrong type",
sourceSet.getName());
} else {
project
.getLogger()
.debug(
"Cannot obtain Kotlin source directory set, sourceSets.{}.convention "
+ "returned unexpected type",
sourceSet.getName());
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
project
.getLogger()
.debug(
"Cannot obtain Kotlin source directory set of source set [{}] via a convention",
sourceSet.getName(),
e);
}
return Optional.empty();
}
}
@@ -0,0 +1,36 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle;
import org.gradle.api.Action;
import org.gradle.api.NamedDomainObjectContainer;
import org.pkl.gradle.spec.ProjectPackageSpec;
import org.pkl.gradle.spec.ProjectResolveSpec;
@SuppressWarnings("unused")
public interface PklProjectCommands {
NamedDomainObjectContainer<ProjectPackageSpec> getPackagers();
NamedDomainObjectContainer<ProjectResolveSpec> getResolvers();
default void packagers(Action<? super NamedDomainObjectContainer<ProjectPackageSpec>> action) {
action.execute(getPackagers());
}
default void resolvers(Action<? super NamedDomainObjectContainer<ProjectResolveSpec>> action) {
action.execute(getResolvers());
}
}
@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.gradle;
import org.pkl.core.util.NonnullByDefault;
@@ -0,0 +1,51 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import java.time.Duration;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
/** Configuration options shared between plugin features. Documented in user manual. */
public interface BasePklSpec {
String getName();
ConfigurableFileCollection getTransitiveModules();
ListProperty<String> getAllowedModules();
ListProperty<String> getAllowedResources();
MapProperty<String, String> getEnvironmentVariables();
MapProperty<String, String> getExternalProperties();
ConfigurableFileCollection getModulePath();
Property<Object> getSettingsModule();
DirectoryProperty getEvalRootDir();
DirectoryProperty getModuleCacheDir();
Property<Boolean> getNoCache();
// use same type (Duration) as Gradle's `Task.timeout`
Property<Duration> getEvalTimeout();
}
@@ -0,0 +1,33 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.SourceSet;
/** Configuration options shared between code generators. Documented in user manual. */
public interface CodeGenSpec extends ModulesSpec {
DirectoryProperty getOutputDir();
Property<SourceSet> getSourceSet();
Property<String> getIndent();
Property<Boolean> getGenerateSpringBootConfig();
Property<Boolean> getImplementSerializable();
}
@@ -0,0 +1,33 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
/** Configuration options for evaluators. Documented in user manual. */
public interface EvalSpec extends ModulesSpec {
RegularFileProperty getOutputFile();
Property<String> getOutputFormat();
Property<String> getModuleOutputSeparator();
DirectoryProperty getMultipleFileOutputDir();
Property<String> getExpression();
}
@@ -0,0 +1,29 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.provider.Property;
/** Configuration options for Java code generators. Documented in user manual. */
public interface JavaCodeGenSpec extends CodeGenSpec {
Property<Boolean> getGenerateGetters();
Property<Boolean> getGenerateJavadoc();
Property<String> getParamsAnnotation();
Property<String> getNonNullAnnotation();
}
@@ -0,0 +1,23 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.provider.Property;
/** Configuration options for Kotlin code generators. Documented in user manual. */
public interface KotlinCodeGenSpec extends CodeGenSpec {
Property<Boolean> getGenerateKdoc();
}
@@ -0,0 +1,33 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
public interface ModulesSpec extends BasePklSpec {
ListProperty<Object> getSourceModules();
ConfigurableFileCollection getTransitiveModules();
DirectoryProperty getProjectDir();
Property<Boolean> getOmitProjectSettings();
Property<Boolean> getNoProject();
}
@@ -0,0 +1,23 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.file.DirectoryProperty;
/** Configuration options for Pkldoc generators. Documented in user manual. */
public interface PkldocSpec extends ModulesSpec {
DirectoryProperty getOutputDir();
}
@@ -0,0 +1,32 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
public interface ProjectPackageSpec extends BasePklSpec {
ConfigurableFileCollection getProjectDirectories();
DirectoryProperty getOutputPath();
DirectoryProperty getJunitReportsDir();
Property<Boolean> getOverwrite();
Property<Boolean> getSkipPublishCheck();
}
@@ -0,0 +1,22 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.file.ConfigurableFileCollection;
public interface ProjectResolveSpec extends BasePklSpec {
ConfigurableFileCollection getProjectDirectories();
}
@@ -0,0 +1,25 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.spec;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
public interface TestSpec extends ModulesSpec {
DirectoryProperty getJunitReportsDir();
Property<Boolean> getOverwrite();
}
@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.gradle.spec;
import org.pkl.core.util.NonnullByDefault;
@@ -0,0 +1,278 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
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.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.LateInit;
import org.pkl.core.util.Nullable;
public abstract class BasePklTask extends DefaultTask {
@Input
public abstract ListProperty<String> getAllowedModules();
@Input
public abstract ListProperty<String> getAllowedResources();
@Input
public abstract MapProperty<String, String> getEnvironmentVariables();
@Input
public abstract MapProperty<String, String> getExternalProperties();
@InputFiles
public abstract ConfigurableFileCollection getModulePath();
@Internal
public abstract Property<Object> getSettingsModule();
@Internal
public Provider<Object> getParsedSettingsModule() {
return getSettingsModule().map(this::parseModuleNotation);
}
@InputFile
@Optional
public Provider<File> getSettingsModuleFile() {
return getParsedSettingsModule()
.map(
it -> {
if (it instanceof File) {
return (File) it;
}
//noinspection DataFlowIssue
return null;
});
}
@Input
@Optional
public Provider<URI> getSettingsModuleUri() {
return getParsedSettingsModule()
.map(
it -> {
if (it instanceof URI) {
return (URI) it;
}
//noinspection DataFlowIssue
return null;
});
}
// Exposed as a task input via evalRootDirPath, because we only need to depend
// on this directory's path and not on its contents.
@Internal
public abstract DirectoryProperty getEvalRootDir();
@Input
@Optional
public Provider<String> getEvalRootDirPath() {
return getEvalRootDir().map(it -> it.getAsFile().getAbsolutePath());
}
// This is not a task input because it doesn't affect task output but only performance.
@Internal
public abstract DirectoryProperty getModuleCacheDir();
@Input
@Optional
public abstract Property<Boolean> getNoCache();
@Input
@Optional
public abstract Property<Duration> getEvalTimeout();
@TaskAction
public void runTask() {
doRunTask();
}
protected abstract void doRunTask();
@LateInit protected CliBaseOptions cachedOptions;
@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(), this::parseModuleNotationToUri),
null,
getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
getNoCache().getOrElse(false),
false,
false,
false,
Collections.emptyList());
}
return cachedOptions;
}
@Internal
protected List<URI> getSourceModulesAsUris() {
return Collections.emptyList();
}
protected List<Path> parseModulePath() {
return getModulePath().getFiles().stream().map(File::toPath).collect(Collectors.toList());
}
/**
* Parses the specified source module notation into a "parsed" notation which is then used for
* input path tracking and as an argument for the CLI API.
*
* <p>This method accepts the following input types:
*
* <ul>
* <li>{@link URI} - used as is.
* <li>{@link File} - used as is.
* <li>{@link Path} - converted to a {@link File}. This conversion may fail because not all
* {@link Path}s point to the local file system.
* <li>{@link URL} - converted to a {@link URI}. This conversion may fail because {@link URL}
* allows for URLs which are not compliant URIs.
* <li>{@link CharSequence} - first, converted to a string. If this string is "URI-like" (see
* {@link IoUtils#isUriLike(String)}), then we attempt to parse it as a {@link URI}, which
* may fail. Otherwise, we attempt to parse it as a {@link Path}, which is then converted to
* a {@link File} (both of these operations may fail).
* <li>{@link FileSystemLocation} - converted to a {@link File} via the {@link
* FileSystemLocation#getAsFile()} method.
* </ul>
*
* In case the returned value is determined to be a {@link URI}, then this URI is first checked
* for whether its scheme is {@code file}, like {@code file:///example/path}. In such case, this
* method returns a {@link File} corresponding to the file path in the URI. Otherwise, a {@link
* URI} instance is returned.
*
* @throws InvalidUserDataException In case the input is none of the types described above, or
* when the underlying value cannot be parsed correctly.
*/
protected Object parseModuleNotation(Object m) {
if (m instanceof URI) {
var u = (URI) m;
if ("file".equals(u.getScheme())) {
return new File(u.getPath());
}
return u;
} else if (m instanceof File) {
return m;
} else if (m instanceof Path) {
try {
return ((Path) m).toFile();
} catch (UnsupportedOperationException e) {
throw new InvalidUserDataException("Failed to parse Pkl module file path: " + m, e);
}
} else if (m instanceof URL) {
try {
return parseModuleNotation(((URL) m).toURI());
} catch (URISyntaxException e) {
throw new InvalidUserDataException("Failed to parse Pkl module URI: " + m, e);
}
} else if (m instanceof CharSequence) {
var s = m.toString();
if (IoUtils.isUriLike(s)) {
try {
return parseModuleNotation(IoUtils.toUri(s));
} catch (URISyntaxException e) {
throw new InvalidUserDataException("Failed to parse Pkl module URI: " + s, e);
}
} else {
try {
return Paths.get(s).toFile();
} catch (InvalidPathException | UnsupportedOperationException e) {
throw new InvalidUserDataException("Failed to parse Pkl module file path: " + s, e);
}
}
} else if (m instanceof FileSystemLocation) {
return ((FileSystemLocation) m).getAsFile();
} else {
throw new InvalidUserDataException(
"Unsupported value of type " + m.getClass() + " used as a module path: " + m);
}
}
protected URI parseModuleNotationToUri(Object m) {
var parsed1 = parseModuleNotation(m);
return parsedModuleNotationToUri(parsed1);
}
/**
* Converts either a file or a URI to a URI. We convert a 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 m) {
if (m instanceof File) {
var f = (File) m;
return IoUtils.createUri(f.getPath());
} else if (m instanceof URI) {
return (URI) m;
}
throw new IllegalArgumentException("Invalid parsed module notation: " + m);
}
protected List<Pattern> patternsFromStrings(List<String> patterns) {
return patterns.stream().map(Pattern::compile).collect(Collectors.toList());
}
/**
* Equivalent to {@code provider.map(it -> f.apply(it)).getOrNull()}.
*
* <p>This function is necessary because in some cases doing {@code
* someProvider.map(...).getOrNull()} may trigger validation errors inside Gradle, when {@code
* someProvider} is derived from a property.
*/
protected <T, U> @Nullable U mapAndGetOrNull(Provider<T> provider, Function<T, U> f) {
@Nullable T value = provider.getOrNull();
return value == null ? null : f.apply(value);
}
}
@@ -0,0 +1,35 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputDirectory;
public abstract class CodeGenTask extends ModulesTask {
@OutputDirectory
public abstract DirectoryProperty getOutputDir();
@Input
public abstract Property<String> getIndent();
@Input
public abstract Property<Boolean> getGenerateSpringBootConfig();
@Input
public abstract Property<Boolean> getImplementSerializable();
}
@@ -0,0 +1,59 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import java.io.File;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.UntrackedTask;
import org.pkl.cli.CliEvaluator;
import org.pkl.cli.CliEvaluatorOptions;
@UntrackedTask(because = "Output file names are known only after execution")
public abstract class EvalTask extends ModulesTask {
@Internal
public abstract RegularFileProperty getOutputFile();
@Internal
public abstract Property<String> getOutputFormat();
@Internal
public abstract Property<String> getModuleOutputSeparator();
@Internal
public abstract DirectoryProperty getMultipleFileOutputDir();
@Internal
public abstract Property<String> getExpression();
@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();
}
}
@@ -0,0 +1,58 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import java.io.File;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.pkl.codegen.java.CliJavaCodeGenerator;
import org.pkl.codegen.java.CliJavaCodeGeneratorOptions;
public abstract class JavaCodeGenTask extends CodeGenTask {
@Input
public abstract Property<Boolean> getGenerateGetters();
@Input
public abstract Property<Boolean> getGenerateJavadoc();
@Input
@Optional
public abstract Property<String> getParamsAnnotation();
@Input
@Optional
public abstract Property<String> getNonNullAnnotation();
@Override
protected void doRunTask() {
//noinspection ResultOfMethodCallIgnored
getOutputs().getPreviousOutputFiles().forEach(File::delete);
new CliJavaCodeGenerator(
new CliJavaCodeGeneratorOptions(
getCliBaseOptions(),
getProject().file(getOutputDir()).toPath(),
getIndent().get(),
getGenerateGetters().get(),
getGenerateJavadoc().get(),
getGenerateSpringBootConfig().get(),
getParamsAnnotation().getOrNull(),
getNonNullAnnotation().getOrNull(),
getImplementSerializable().get()))
.run();
}
}
@@ -0,0 +1,43 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import java.io.File;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.pkl.codegen.kotlin.CliKotlinCodeGenerator;
import org.pkl.codegen.kotlin.CliKotlinCodeGeneratorOptions;
public abstract class KotlinCodeGenTask extends CodeGenTask {
@Input
public abstract Property<Boolean> getGenerateKdoc();
@Override
protected void doRunTask() {
//noinspection ResultOfMethodCallIgnored
getOutputs().getPreviousOutputFiles().forEach(File::delete);
new CliKotlinCodeGenerator(
new CliKotlinCodeGeneratorOptions(
getCliBaseOptions(),
getProject().file(getOutputDir()).toPath(),
getIndent().get(),
getGenerateKdoc().get(),
getGenerateSpringBootConfig().get(),
getImplementSerializable().get()))
.run();
}
}
@@ -0,0 +1,185 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import java.io.File;
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;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Pair;
public abstract class ModulesTask extends BasePklTask {
// We expose the contents of this property as task inputs via the sourceModuleFiles
// and sourceModuleUris properties. We cannot use two separate properties because
// the order of source modules matters for the CLI API invocation, so it must be
// a single collection.
@Internal
public abstract ListProperty<Object> getSourceModules();
@InputFiles
public abstract ConfigurableFileCollection getTransitiveModules();
private final Map<List<Object>, Pair<List<File>, List<URI>>> parsedSourceModulesCache =
new HashMap<>();
// Used for input tracking purposes only.
@Internal
public Provider<Pair<List<File>, List<URI>>> getParsedSourceModules() {
return getSourceModules()
.map(it -> parsedSourceModulesCache.computeIfAbsent(it, this::splitFilesAndUris));
}
// We use @InputFiles and FileCollection here to ensure that file contents are tracked.
@InputFiles
public FileCollection getSourceModuleFiles() {
return getProject().files(getParsedSourceModules().map(it -> it.first));
}
// We use @Input and just a list value because we can only track the URIs themselves
// but not their contents.
@Input
public Provider<List<URI>> getSourceModuleUris() {
return getParsedSourceModules().map(it -> it.second);
}
/**
* Returns the sourceModules property as a list of URIs.
*
* <p>This method ensures that the order of source modules in the sourceModules property is
* preserved all the way to the CLI API invocation.
*/
@Internal
@Override
protected List<URI> getSourceModulesAsUris() {
return getSourceModules().get().stream()
.map(this::parseModuleNotationToUri)
.collect(Collectors.toList());
}
// Exposed as a task input via getProjectDirPath, because we only need to depend
// on this directory's path and not on its contents.
@Internal
public abstract DirectoryProperty getProjectDir();
@Input
@Optional
public Provider<String> getProjectDirPath() {
return getProjectDir().map(it -> it.getAsFile().getAbsolutePath());
}
@Input
@Optional
public abstract Property<Boolean> getOmitProjectSettings();
@Input
@Optional
public abstract Property<Boolean> getNoProject();
/**
* A source module can be either a file or a URI. Files can be tracked, so this method splits a
* collection of module notations (which can be strings, URIs, URLs, Files or Paths) into a list
* of files (for content-based tracking) and URIs (for simple value-based tracking). These lists
* are then exposed as separate read-only properties to make Gradle track them as proper inputs.
*/
private Pair<List<File>, List<URI>> splitFilesAndUris(List<Object> modules) {
var files = new ArrayList<File>();
var uris = new ArrayList<URI>();
for (var m : modules) {
var parsed = parseModuleNotation(m);
if (parsed instanceof File) {
files.add((File) parsed);
} else if (parsed instanceof URI) {
uris.add((URI) parsed);
}
}
return Pair.of(files, uris);
}
/**
* Converts either a file or a URI to a URI. We convert a 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 m) {
if (m instanceof File) {
var f = (File) m;
return IoUtils.createUri(f.getPath());
} else if (m instanceof URI) {
return (URI) m;
}
throw new IllegalArgumentException("Invalid parsed module notation: " + m);
}
protected URI parseModuleNotationToUri(Object m) {
var parsed1 = parseModuleNotation(m);
return parsedModuleNotationToUri(parsed1);
}
@TaskAction
@Override
public void runTask() {
if (getCliBaseOptions().getNormalizedSourceModules().isEmpty()) {
throw new InvalidUserDataException("No source modules specified.");
}
doRunTask();
}
@Internal
@Override
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(), this::parseModuleNotationToUri),
getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null,
getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
getNoCache().getOrElse(false),
getOmitProjectSettings().getOrElse(false),
getNoProject().getOrElse(false),
false,
Collections.emptyList());
}
return cachedOptions;
}
}
@@ -0,0 +1,34 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.tasks.OutputDirectory;
import org.pkl.doc.CliDocGenerator;
import org.pkl.doc.CliDocGeneratorOptions;
public abstract class PkldocTask extends ModulesTask {
@OutputDirectory
public abstract DirectoryProperty getOutputDir();
@Override
protected void doRunTask() {
new CliDocGenerator(
new CliDocGeneratorOptions(
getCliBaseOptions(), getOutputDir().get().getAsFile().toPath()))
.run();
}
}
@@ -0,0 +1,74 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.util.stream.Collectors;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.UntrackedTask;
import org.pkl.cli.CliProjectPackager;
import org.pkl.commons.cli.CliTestOptions;
@UntrackedTask(because = "Output names are known only after execution")
public abstract class ProjectPackageTask extends BasePklTask {
@InputFiles
public abstract ConfigurableFileCollection getProjectDirectories();
@Internal
public abstract DirectoryProperty getOutputPath();
@Optional
@OutputDirectory
public abstract DirectoryProperty getJunitReportsDir();
@Input
public abstract Property<Boolean> getOverwrite();
@Input
@Optional
public abstract Property<Boolean> getSkipPublishCheck();
@Override
protected void doRunTask() {
var projectDirectories =
getProjectDirectories().getFiles().stream()
.map(it -> Path.of(it.getAbsolutePath()))
.collect(Collectors.toList());
if (projectDirectories.isEmpty()) {
throw new InvalidUserDataException("No project directories specified.");
}
new CliProjectPackager(
getCliBaseOptions(),
projectDirectories,
new CliTestOptions(
mapAndGetOrNull(getJunitReportsDir(), it -> it.getAsFile().toPath()),
getOverwrite().get()),
getOutputPath().get().getAsFile().getAbsolutePath(),
getSkipPublishCheck().getOrElse(false),
new PrintWriter(System.out),
new PrintWriter(System.err))
.run();
}
}
@@ -0,0 +1,63 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import java.io.File;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.pkl.cli.CliProjectResolver;
public abstract class ProjectResolveTask extends BasePklTask {
@Internal
public abstract ConfigurableFileCollection getProjectDirectories();
// Only the `PklProject` files matter for creating PklProject.deps.json files.
// Otherwise, these tasks can be considered up to date.
@InputFiles
public Provider<List<File>> getProjectPklFiles() {
return getProjectDirectories()
.getElements()
.map(
(files) ->
files.stream()
.map((it) -> it.getAsFile().toPath().resolve("PklProject").toFile())
.collect(Collectors.toList()));
}
@Override
protected void doRunTask() {
var projectDirectories =
getProjectDirectories().getFiles().stream()
.map(it -> Path.of(it.getAbsolutePath()))
.collect(Collectors.toList());
if (projectDirectories.isEmpty()) {
throw new InvalidUserDataException("No project directories specified.");
}
new CliProjectResolver(
getCliBaseOptions(),
projectDirectories,
new PrintWriter(System.out),
new PrintWriter(System.err))
.run();
}
}
@@ -0,0 +1,46 @@
/**
* Copyright © 2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.gradle.task;
import java.io.PrintWriter;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.pkl.cli.CliTestRunner;
import org.pkl.commons.cli.CliTestOptions;
public abstract class TestTask extends ModulesTask {
@Optional
@OutputDirectory
public abstract DirectoryProperty getJunitReportsDir();
@Input
public abstract Property<Boolean> getOverwrite();
@Override
protected void doRunTask() {
new CliTestRunner(
getCliBaseOptions(),
new CliTestOptions(
mapAndGetOrNull(getJunitReportsDir(), it -> it.getAsFile().toPath()),
getOverwrite().get()),
new PrintWriter(System.out),
new PrintWriter(System.err))
.run();
}
}
@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.gradle.task;
import org.pkl.core.util.NonnullByDefault;
@@ -0,0 +1,69 @@
package org.pkl.gradle
import org.pkl.commons.createParentDirectories
import org.pkl.commons.readString
import org.pkl.commons.writeString
import org.assertj.core.api.Assertions.assertThat
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.UnexpectedBuildFailure
import org.junit.jupiter.api.io.TempDir
import java.net.URI
import java.nio.file.Path
abstract class AbstractTest {
private val gradleVersion: String? = System.getProperty("testGradleVersion")
private val gradleDistributionUrl: String? = System.getProperty("testGradleDistributionUrl")
@TempDir
protected lateinit var testProjectDir: Path
protected fun runTask(
taskName: String,
expectFailure: Boolean = false
): BuildResult {
val runner = GradleRunner.create()
.withProjectDir(testProjectDir.toFile())
.withArguments("--stacktrace", "--no-build-cache", taskName)
.withPluginClasspath()
.withDebug(true)
if (gradleVersion != null) {
runner.withGradleVersion(gradleVersion)
}
if (gradleDistributionUrl != null) {
runner.withGradleDistribution(URI(gradleDistributionUrl))
}
return try {
if (expectFailure) runner.buildAndFail() else runner.build()
} catch (e: UnexpectedBuildFailure) {
throw AssertionError(e.buildResult.output)
}
}
protected fun writeFile(fileName: String, contents: String): Path {
return testProjectDir.resolve(fileName)
.apply { createParentDirectories() }
.writeString(contents.trimIndent())
}
protected fun checkFileContents(file: Path, contents: String) {
assertThat(file).exists()
assertThat(file.readString().trim())
.isEqualTo(contents.trim())
}
protected fun checkTextContains(text: String, vararg contents: String) {
for (content in contents) {
try {
assertThat(text).contains(content.trimMargin())
} catch (e: AssertionError) {
// to get diff output in IDE
assertThat(text).isEqualTo(content.trimMargin())
}
}
}
}
@@ -0,0 +1,505 @@
package org.pkl.gradle
import org.assertj.core.api.Assertions
import org.pkl.commons.readString
import org.pkl.commons.test.PackageServer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
class EvaluatorsTest : AbstractTest() {
@Test
fun `render Pcf`() {
writeBuildFile("pcf")
writePklFile()
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.pcf")
checkFileContents(
outputFile, """
person {
name = "Pigeon"
age = 30
}
""".trimIndent()
)
}
@Test
fun `render YAML`() {
writeBuildFile("yaml")
writePklFile()
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.yaml")
checkFileContents(
outputFile, """
person:
name: Pigeon
age: 30
""".trimIndent()
)
}
@Test
fun `render JSON`() {
writeBuildFile("json")
writePklFile()
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.json")
checkFileContents(
outputFile, """
{
"person": {
"name": "Pigeon",
"age": 30
}
}
""".trimIndent()
)
}
@Test
fun `render plist`() {
writeBuildFile("plist")
writePklFile()
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.plist")
checkFileContents(
outputFile, """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>person</key>
<dict>
<key>name</key>
<string>Pigeon</string>
<key>age</key>
<integer>30</integer>
</dict>
</dict>
</plist>
""".trimIndent()
)
}
@Test
fun `set external properties`() {
writeBuildFile(
"pcf", """
externalProperties = [prop1: "value1", prop2: "value2"]
""".trimIndent()
)
writePklFile(
"""
prop1 = read("prop:prop1")
prop2 = read("prop:prop2")
other = read?("prop:other")
""".trimIndent()
)
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.pcf")
checkFileContents(
outputFile, """
prop1 = "value1"
prop2 = "value2"
other = null
""".trimIndent()
)
}
@Test
fun `defaults to empty environment variables`() {
writeBuildFile("pcf")
writePklFile(
"""
prop1 = read?("env:USER")
prop2 = read?("env:PATH")
prop3 = read?("env:JAVA_HOME")
""".trimIndent()
)
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.pcf")
checkFileContents(
outputFile, """
prop1 = null
prop2 = null
prop3 = null
""".trimIndent()
)
}
@Test
fun `set environment variables`() {
writeBuildFile(
"pcf", """
environmentVariables = [VAR1: "value1", VAR2: "value2"]
""".trimIndent()
)
writePklFile(
"""
prop1 = read("env:VAR1")
prop2 = read("env:VAR2")
other = read?("env:OTHER")
""".trimIndent()
)
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.pcf")
checkFileContents(
outputFile, """
prop1 = "value1"
prop2 = "value2"
other = null
""".trimIndent()
)
}
@Test
fun `no source modules`() {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
evaluators {
evalTest {
outputFormat = "pcf"
}
}
}
"""
)
val result = runTask("evalTest", true)
assertThat(result.output).contains("No source modules specified.")
}
@Test
fun `source module URIs`() {
val pklFile = writeFile(
"test.pkl", """
person {
name = "Pigeon"
age = 20 + 10
}
"""
)
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
evaluators {
evalTest {
sourceModules = [uri("modulepath:/test.pkl")]
modulePath.from "${pklFile.parent}"
outputFile = layout.projectDirectory.file("test.pcf")
settingsModule = "pkl:settings"
}
}
}
"""
)
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.pcf")
checkFileContents(
outputFile, """
person {
name = "Pigeon"
age = 30
}
""".trimIndent()
)
}
@Test
fun `cannot evaluate module located outside evalRootDir`() {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
evaluators {
evalTest {
evalRootDir = file("/non/existing")
sourceModules = ["test.pkl"]
settingsModule = "pkl:settings"
}
}
}
"""
)
val result = runTask("evalTest", expectFailure = true)
assertThat(result.output).contains("Refusing to load module")
assertThat(result.output).contains("because it does not match any entry in the module allowlist (`--allowed-modules`).")
}
@Test
fun `evaluation timeout`() {
// Gradle 4.10 doesn't automatically import Duration
writeBuildFile(
"pcf", """
evalTimeout = java.time.Duration.ofMillis(100)
"""
)
writePklFile(
"""
function fib(n) = if (n < 2) 0 else fib(n - 1) + fib(n - 2)
x = fib(100)
"""
)
val result = runTask("evalTest", expectFailure = true)
assertThat(result.output).contains("timed out")
}
@Test
fun `module output separator`() {
val outputFile = testProjectDir.resolve("test.pcf")
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
evaluators {
evalTask {
moduleOutputSeparator = "// hello"
sourceModules = ["test1.pkl", "test2.pkl"]
settingsModule = "pkl:settings"
outputFile = layout.projectDirectory.file("test.pcf")
}
}
}
"""
)
writeFile(
"test1.pkl",
"foo = 1"
)
writeFile(
"test2.pkl",
"bar = 2"
)
runTask("evalTask")
checkFileContents(outputFile, """
foo = 1
// hello
bar = 2
""".trimIndent())
}
@Test
fun `compliant file URIs`() {
writeBuildFile("pcf")
writeFile("test.pkl", """
import "pkl:reflect"
output {
text = reflect.Module(module).uri
}
""".trimIndent())
runTask("evalTest")
val outputFile = testProjectDir.resolve("test.pcf")
assertThat(outputFile).exists()
assertThat(outputFile.readString().trim()).startsWith("file:///")
}
@Test
fun `multiple file output`() {
writeBuildFile("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"
}
}
}
""".trimIndent()
)
runTask("evalTest")
checkFileContents(testProjectDir.resolve("my-output/output-1.txt"), "My output 1")
checkFileContents(testProjectDir.resolve("my-output/output-2.txt"), "My output 2")
}
@Test
fun expression() {
writeBuildFile("yaml", """
expression = "metadata.name"
outputFile = layout.projectDirectory.file("output.txt")
""".trimIndent())
writeFile(
"test.pkl",
"""
metadata {
name = "Uni"
}
""".trimIndent()
)
runTask("evalTest")
checkFileContents(testProjectDir.resolve("output.txt"), "Uni")
}
@Test
fun `explicitly set cache dir`(@TempDir tempDir: Path) {
writeBuildFile("pcf", """
moduleCacheDir = file("$tempDir")
""".trimIndent())
writeFile(
"test.pkl",
"""
import "package://localhost:12110/birds@0.5.0#/Bird.pkl"
res = new Bird { name = "Wally"; favoriteFruit { name = "bananas" } }
""".trimIndent()
)
PackageServer.populateCacheDir(tempDir)
runTask("evalTest")
}
@Test
fun `explicitly set project dir`() {
writeBuildFile("pcf", "projectDir = file(\"proj1\")", listOf("proj1/foo.pkl"))
writeFile("proj1/PklProject", """
amends "pkl:Project"
dependencies {
["proj2"] = import("../proj2/PklProject")
}
package {
name = "proj1"
baseUri = "package://localhost:12110/\(name)"
version = "1.0.0"
packageZipUrl = "https://localhost:12110/\(name)@\(version).zip"
}
""".trimIndent())
writeFile("proj2/PklProject", """
amends "pkl:Project"
package {
name = "proj2"
baseUri = "package://localhost:12110/\(name)"
version = "1.0.0"
packageZipUrl = "https://localhost:12110/\(name)@\(version).zip"
}
""".trimIndent())
writeFile("proj1/PklProject.deps.json", """
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/proj2@1": {
"type": "local",
"uri": "projectpackage://localhost:12110/proj2@1.0.0",
"path": "../proj2"
}
}
}
""".trimIndent())
writeFile("proj2/PklProject.deps.json", """
{
"schemaVersion": 1,
"resolvedDependencies": {}
}
""".trimIndent())
writeFile("proj1/foo.pkl", """
module proj1.foo
bar: String = import("@proj2/baz.pkl").qux
""".trimIndent())
writeFile("proj2/baz.pkl", """
qux: String = "Contents of @proj2/qux"
""".trimIndent())
runTask("evalTest")
assertThat(testProjectDir.resolve("proj1/foo.pcf")).exists()
}
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")
) {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
evaluators {
evalTest {
sourceModules = [${sourceModules.joinToString(separator = ", ") { "\"$it\"" }}]
outputFormat = "$outputFormat"
settingsModule = "pkl:settings"
$additionalContents
}
}
}
"""
)
}
private fun writePklFile(
contents: String = """
person {
name = "Pigeon"
age = 20 + 10
}
"""
) {
writeFile("test.pkl", contents)
}
}
@@ -0,0 +1,168 @@
package org.pkl.gradle
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.readText
class JavaCodeGeneratorsTest : AbstractTest() {
@Test
fun `generate code`() {
writeBuildFile()
writePklFile()
runTask("configClasses")
val baseDir = testProjectDir.resolve("build/generated/java/org")
val moduleFile = baseDir.resolve("Mod.java")
assertThat(baseDir.listDirectoryEntries().count()).isEqualTo(1)
assertThat(moduleFile).exists()
val text = moduleFile.readText()
// shading must not affect generated code
assertThat(text).doesNotContain("org.pkl.thirdparty")
checkTextContains(
text, """
|public final class Mod {
| public final @Nonnull Object other;
"""
)
checkTextContains(
text, """
| public static final class Person {
| public final @Nonnull String name;
|
| public final @Nonnull List<@Nonnull Address> addresses;
"""
)
checkTextContains(
text, """
| public static final class Address {
| public final @Nonnull String street;
|
| public final long zip;
"""
)
}
@Test
fun `compile generated code`() {
writeBuildFile()
writeFile("mod.pkl", """
module org.mod
class Person {
name: String
addresses: List<Address?>
}
class Address {
street: String
zip: Int
}
other: Any = 42
""".trimIndent())
runTask("compileJava")
val classesDir = testProjectDir.resolve("build/classes/java/main")
val moduleClassFile = classesDir.resolve("org/Mod.class")
val personClassFile = classesDir.resolve("org/Mod\$Person.class")
val addressClassFile = classesDir.resolve("org/Mod\$Address.class")
assertThat(moduleClassFile).exists()
assertThat(personClassFile).exists()
assertThat(addressClassFile).exists()
}
@Test
fun `no source modules`() {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
evaluators {
evalTest {
outputFormat = "pcf"
}
}
}
"""
)
val result = runTask("evalTest", true)
assertThat(result.output).contains("No source modules specified.")
}
private fun writeBuildFile() {
writeFile(
"build.gradle", """
plugins {
id "java"
id "org.pkl-lang"
}
repositories {
mavenCentral()
}
dependencies {
implementation "javax.inject:javax.inject:1"
implementation "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"
}
}
}
"""
)
}
private fun writeGradlePropertiesFile() {
writeFile("gradle.properties", """
systemProp.http.proxyHost=proxy.config.pcp.local
systemProp.http.proxyPort=3128
systemProp.http.nonProxyHosts=localhost|*.apple.com
systemProp.https.proxyHost=proxy.config.pcp.local
systemProp.https.proxyPort=3128
systemProp.https.nonProxyHosts=localhost|*.apple.com
""")
}
private fun writePklFile() {
writeFile(
"mod.pkl", """
module org.mod
class Person {
name: String
addresses: List<Address>
}
class Address {
street: String
zip: Int
}
other = 42
"""
)
}
}
@@ -0,0 +1,155 @@
package org.pkl.gradle
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.readText
class KotlinCodeGeneratorsTest : AbstractTest() {
@Test
fun `generate code`() {
writeBuildFile()
writePklFile()
runTask("configClasses")
val baseDir = testProjectDir.resolve("build/generated/kotlin/org")
val kotlinFile = baseDir.resolve("Mod.kt")
assertThat(baseDir.listDirectoryEntries().count()).isEqualTo(1)
assertThat(kotlinFile).exists()
val text = kotlinFile.readText()
// shading must not affect generated code
assertThat(text).doesNotContain("org.pkl.thirdparty")
checkTextContains(
text, """
|data class Mod(
| val other: Any?
|)
"""
)
checkTextContains(
text, """
| data class Person(
| val name: String,
| val addresses: List<Address>
| )
"""
)
checkTextContains(
text, """
| open class Address(
| open val street: String,
| open val zip: Long
| )
"""
)
}
@Test
fun `compile generated code`() {
writeBuildFile()
writePklFile()
runTask("compileKotlin")
val classesDir = testProjectDir.resolve("build/classes/kotlin/main")
val moduleClassFile = classesDir.resolve("org/Mod.class")
val personClassFile = classesDir.resolve("org/Mod\$Person.class")
val addressClassFile = classesDir.resolve("org/Mod\$Address.class")
assertThat(moduleClassFile).exists()
assertThat(personClassFile).exists()
assertThat(addressClassFile).exists()
}
@Test
fun `no source modules`() {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
evaluators {
evalTest {
outputFormat = "pcf"
}
}
}
"""
)
val result = runTask("evalTest", true)
assertThat(result.output).contains("No source modules specified.")
}
private fun writeBuildFile() {
val kotlinVersion = "1.6.0"
writeFile(
"build.gradle", """
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") {
exclude module: "kotlin-android-extensions"
}
}
}
plugins {
id "org.pkl-lang"
}
apply plugin: "kotlin"
repositories {
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
}
pkl {
kotlinCodeGenerators {
configClasses {
sourceModules = ["mod.pkl"]
outputDir = file("build/generated")
settingsModule = "pkl:settings"
}
}
}
"""
)
}
private fun writePklFile() {
writeFile(
"mod.pkl", """
module org.mod
class Person {
name: String
addresses: List<Address>
}
// "open" to test generating regular class
open class Address {
street: String
zip: Int
}
other = 42
"""
)
}
}
@@ -0,0 +1,99 @@
package org.pkl.gradle
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.io.path.readText
class PkldocGeneratorsTest : AbstractTest() {
@Test
fun `generate docs`() {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
pkldocGenerators {
pkldoc {
sourceModules = ["person.pkl", "doc-package-info.pkl"]
outputDir = file("build/pkldoc")
settingsModule = "pkl:settings"
}
}
}
"""
)
writeFile(
"doc-package-info.pkl", """
/// A test package.
amends "pkl:DocPackageInfo"
name = "test"
version = "1.0.0"
importUri = "https://pkl-lang.org/"
authors { "publisher@apple.com" }
sourceCode = "sources.apple.com/"
issueTracker = "issues.apple.com"
""".trimIndent()
)
writeFile(
"person.pkl", """
module test.person
class Person {
name: String
addresses: List<Address>
}
class Address {
street: String
zip: Int
}
other = 42
""".trimIndent()
)
runTask("pkldoc")
val baseDir = testProjectDir.resolve("build/pkldoc")
val mainFile = baseDir.resolve("index.html")
val packageFile = baseDir.resolve("test/1.0.0/index.html")
val moduleFile = baseDir.resolve("test/1.0.0/person/index.html")
val personFile = baseDir.resolve("test/1.0.0/person/Person.html")
val addressFile = baseDir.resolve("test/1.0.0/person/Address.html")
assertThat(mainFile).exists()
assertThat(packageFile).exists()
assertThat(moduleFile).exists()
assertThat(personFile).exists()
assertThat(addressFile).exists()
checkTextContains(mainFile.readText(), "<html>", "test")
checkTextContains(packageFile.readText(), "<html>", "test.person")
checkTextContains(moduleFile.readText(), "<html>", "Person", "Address", "other")
checkTextContains(personFile.readText(), "<html>", "name", "addresses")
checkTextContains(addressFile.readText(), "<html>", "street", "zip")
}
@Test
fun `no source modules`() {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
pkldocGenerators {
pkldoc {
}
}
}
""".trimIndent()
)
val result = runTask("pkldoc", true)
assertThat(result.output).contains("No source modules specified.")
}
}
@@ -0,0 +1,98 @@
package org.pkl.gradle
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class ProjectPackageTest : AbstractTest() {
@Test
fun basic() {
writeBuildFile("skipPublishCheck.set(true)")
writeProjectContent()
runTask("createMyPackages")
assertThat(testProjectDir.resolve("build/generated/pkl/packages/proj1@1.0.0.zip")).exists()
assertThat(testProjectDir.resolve("build/generated/pkl/packages/proj1@1.0.0")).exists()
}
@Test
fun `custom output dir`() {
writeBuildFile( """
outputPath.set(file("thepackages"))
skipPublishCheck.set(true)
""")
writeProjectContent()
runTask("createMyPackages")
assertThat(testProjectDir.resolve("thepackages/proj1@1.0.0.zip")).exists()
assertThat(testProjectDir.resolve("thepackages/proj1@1.0.0")).exists()
}
@Test
fun `junit dir`() {
writeBuildFile("""
junitReportsDir.set(file("test-reports"))
skipPublishCheck.set(true)
""".trimIndent())
writeProjectContent()
runTask("createMyPackages")
assertThat(testProjectDir.resolve("test-reports")).isNotEmptyDirectory()
}
private fun writeBuildFile(additionalContents: String = "") {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
project {
packagers {
createMyPackages {
projectDirectories.from(file("proj1"))
settingsModule = "pkl:settings"
$additionalContents
}
}
}
}
"""
)
}
private fun writeProjectContent() {
writeFile("proj1/PklProject", """
amends "pkl:Project"
package {
name = "proj1"
baseUri = "package://localhost:12110/proj1"
version = "1.0.0"
packageZipUrl = "https://localhost:12110/proj1@\(version).zip"
apiTests {
"tests.pkl"
}
}
""".trimIndent())
writeFile("proj1/PklProject.deps.json", """
{
"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")
}
}
@@ -0,0 +1,47 @@
package org.pkl.gradle
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class ProjectResolveTest : AbstractTest() {
@Test
fun basic() {
writeBuildFile()
writeProjectContent()
runTask("resolveMyProj")
assertThat(testProjectDir.resolve("proj1/PklProject.deps.json")).hasContent("""
{
"schemaVersion": 1,
"resolvedDependencies": {}
}
""".trimIndent())
}
private fun writeBuildFile(additionalContents: String = "") {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
project {
resolvers {
resolveMyProj {
projectDirectories.from(file("proj1"))
settingsModule = "pkl:settings"
$additionalContents
}
}
}
}
"""
)
}
private fun writeProjectContent() {
writeFile("proj1/PklProject", """
amends "pkl:Project"
""".trimIndent())
}
}
@@ -0,0 +1,301 @@
package org.pkl.gradle
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import java.nio.file.Path
import kotlin.io.path.readText
class TestsTest : AbstractTest() {
@Test
fun `facts pass`() {
writeBuildFile()
writePklFile()
val res = runTask("evalTest")
assertThat(res.output).contains("should pass ✅")
}
@Test
fun `facts fail`() {
writeBuildFile()
writePklFile(additionalFacts = """
["should fail"] {
1 == 3
"foo" == "bar"
}
""".trimIndent())
val res = runTask("evalTest", expectFailure = true)
assertThat(res.output).contains("should fail ❌")
assertThat(res.output).contains("1 == 3 ❌")
assertThat(res.output).contains(""""foo" == "bar" ❌""")
}
@Test
fun error() {
writeBuildFile()
writePklFile(additionalFacts = """
["error"] {
throw("exception")
}
""".trimIndent())
val output = runTask("evalTest", expectFailure = true).output.stripFilesAndLines()
assertThat(output.trimStart()).startsWith("""
> Task :evalTest FAILED
module test (file:///file, line x)
test ❌
Error:
–– Pkl Error ––
exception
9 | throw("exception")
^^^^^^^^^^^^^^^^^^
at test#facts["error"][#1] (file:///file, line x)
3 | facts {
^^^^^^^
at test#facts (file:///file, line x)
""".trimIndent())
}
@Test
fun `full example`() {
writePklFile(contents = bigTest)
writeFile("test.pkl-expected.pcf", bigTestExpected)
writeBuildFile()
val output = runTask("evalTest", expectFailure = true).output.stripFilesAndLines()
assertThat(output.trimStart()).contains("""
module test (file:///file, line x)
sum numbers ✅
divide numbers ✅
fail ❌
4 == 9 ❌ (file:///file, line x)
"foo" == "bar" ❌ (file:///file, line x)
user 0 ✅
user 1 ❌
(file:///file, line x)
Expected: (file:///file, line x)
new {
name = "Pigeon"
age = 40
}
Actual: (file:///file, line x)
new {
name = "Pigeon"
age = 41
}
user 1 ❌
(file:///file, line x)
Expected: (file:///file, line x)
new {
name = "Parrot"
age = 35
}
Actual: (file:///file, line x)
new {
name = "Welma"
age = 35
}
""".trimIndent())
}
@Test
fun `overwrite expected examples`() {
writePklFile(additionalExamples = """
["user 0"] {
new {
name = "Cool"
age = 11
}
}
["user 1"] {
new {
name = "Pigeon"
age = 41
}
new {
name = "Welma"
age = 35
}
}
""".trimIndent())
writeFile("test.pkl-expected.pcf", bigTestExpected)
writeBuildFile("overwrite = true")
val output = runTask("evalTest").output
assertThat(output).contains("user 0 ✍️")
assertThat(output).contains("user 1 ✍️")
}
@Test
fun `JUnit reports`() {
val pklFile = writePklFile(contents = bigTest)
writeFile("test.pkl-expected.pcf", bigTestExpected)
writeBuildFile("junitReportsDir = file('${pklFile.parent}/build')")
runTask("evalTest", expectFailure = true)
val outputFile = testProjectDir.resolve("build/test.xml")
val report = outputFile.readText().stripFilesAndLines()
assertThat(report).isEqualTo("""
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="test" tests="6" failures="4">
<testcase classname="test" name="sum numbers"></testcase>
<testcase classname="test" name="divide numbers"></testcase>
<testcase classname="test" name="fail">
<failure message="Fact Failure">4 == 9 ❌ (file:///file, line x)</failure>
<failure message="Fact Failure">&quot;foo&quot; == &quot;bar&quot; ❌ (file:///file, line x)</failure>
</testcase>
<testcase classname="test" name="user 0"></testcase>
<testcase classname="test" name="user 1">
<failure message="Example Failure">(file:///file, line x)
Expected: (file:///file, line x)
new {
name = &quot;Pigeon&quot;
age = 40
}
Actual: (file:///file, line x)
new {
name = &quot;Pigeon&quot;
age = 41
}</failure>
</testcase>
<testcase classname="test" name="user 1">
<failure message="Example Failure">(file:///file, line x)
Expected: (file:///file, line x)
new {
name = &quot;Parrot&quot;
age = 35
}
Actual: (file:///file, line x)
new {
name = &quot;Welma&quot;
age = 35
}</failure>
</testcase>
<system-err><![CDATA[8 = 8
]]></system-err>
</testsuite>
""".trimIndent())
}
private val bigTest = """
amends "pkl:test"
local function sum(a, b) = a + b
facts {
["sum numbers"] {
sum(3, 5) == trace(8)
sum(3, 0) == 3
}
["divide numbers"] {
(8 / 4) == 2
(12 / 2) == 6
}
["fail"] {
4 == 9
"foo" == "bar"
}
}
examples {
["user 0"] {
new {
name = "Cool"
age = 11
}
}
["user 1"] {
new {
name = "Pigeon"
age = 41
}
new {
name = "Welma"
age = 35
}
}
}
""".trimIndent()
private val bigTestExpected = """
examples {
["user 0"] {
new {
name = "Cool"
age = 11
}
}
["user 1"] {
new {
name = "Pigeon"
age = 40
}
new {
name = "Parrot"
age = 35
}
}
}
""".trimIndent()
private fun writeBuildFile(additionalContents: String = "") {
writeFile(
"build.gradle", """
plugins {
id "org.pkl-lang"
}
pkl {
tests {
evalTest {
sourceModules = ["test.pkl"]
settingsModule = "pkl:settings"
$additionalContents
}
}
}
"""
)
}
private fun writePklFile(
additionalFacts: String = "",
additionalExamples: String = "",
contents: String = """
amends "pkl:test"
facts {
["should pass"] {
1 == 1
10 == 10
}
$additionalFacts
}
examples {
$additionalExamples
}
"""
): Path {
return writeFile("test.pkl", contents)
}
private fun String.stripFilesAndLines(): String =
replace(Regex("""\(file:///.*, line \d+\)"""), "(file:///file, line x)")
}