From 92d2db4ed6ba362418fc20e4aa1a166deb5c3fe2 Mon Sep 17 00:00:00 2001 From: "R. C. Howell" Date: Thu, 14 Jul 2022 16:31:04 -0700 Subject: [PATCH] Creates PIG Gradle plugin --- pig-gradle-plugin/README.md | 44 +++++++++ pig-gradle-plugin/build.gradle | 69 ++++++++++++++ .../org/partiql/pig/plugin/PigExtension.kt | 33 +++++++ .../src/org/partiql/pig/plugin/PigPlugin.kt | 89 +++++++++++++++++++ .../src/org/partiql/pig/plugin/PigTask.kt | 86 ++++++++++++++++++ pig-tests/build.gradle | 66 +++++--------- settings.gradle | 6 ++ 7 files changed, 350 insertions(+), 43 deletions(-) create mode 100644 pig-gradle-plugin/README.md create mode 100644 pig-gradle-plugin/build.gradle create mode 100644 pig-gradle-plugin/src/org/partiql/pig/plugin/PigExtension.kt create mode 100644 pig-gradle-plugin/src/org/partiql/pig/plugin/PigPlugin.kt create mode 100644 pig-gradle-plugin/src/org/partiql/pig/plugin/PigTask.kt diff --git a/pig-gradle-plugin/README.md b/pig-gradle-plugin/README.md new file mode 100644 index 0000000..08007ff --- /dev/null +++ b/pig-gradle-plugin/README.md @@ -0,0 +1,44 @@ +# PIG Gradle Plugin + +```groovy +plugins { + id 'pig-gradle-plugin' +} +``` + +## Source Sets +```groovy +sourceSets { + main { + pig { + // in addition to the default 'src/main/pig' + srcDir 'path/to/type/universes' + } + } + test { + pig { + // in addition to the default 'src/test/pig' + srcDir 'path/to/test/type/universes' + } + } +} +``` + +## Tasks + +``` +pig { + target = "kotlin" // required + namespace = ... // optional + template = ... // optional + outDir = 'build/generated-sources/pig/' // default +} +``` + +## Dependencies + +| Task Name | Depends On | +|---------------------------|-------------------------------| +| compileJava | generatePigSource | +| compileTestJava | generatePigTestSource | +| compile*SourceSet*Java | generatePig*SourceSet*Source | diff --git a/pig-gradle-plugin/build.gradle b/pig-gradle-plugin/build.gradle new file mode 100644 index 0000000..d7d80bb --- /dev/null +++ b/pig-gradle-plugin/build.gradle @@ -0,0 +1,69 @@ +plugins { + id 'java-gradle-plugin' + id 'org.jetbrains.kotlin.jvm' version '1.4.0' + id 'com.gradle.plugin-publish' version '1.0.0' +} + +repositories { + mavenCentral() +} + +dependencies { + // It is non-trivial to depend on a local plugin within a gradle project + // The simplest way is using a composite build: https://docs.gradle.org/current/userguide/composite_builds.html + // Other methods involved adding the build/lib/... jar to classpath, or publish to maven local + // By adding the plugin as a dep in `pig-tests`, I cannot use an included build of `pig` in the plugin + // Hence it's much simpler to use the latest published version in the plugin + implementation 'org.partiql:partiql-ir-generator:0.5.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' +} + +test { + useJUnitPlatform() +} + +pluginBundle { + website = 'https://github.com/partiql/partiql-ir-generator/wiki' + vcsUrl = 'https://github.com/partiql/partiql-ir-generator' + tags = ['partiql', 'pig', 'ir', 'partiql-ir-generator'] +} + +gradlePlugin { + plugins { + create('pig-gradle-plugin') { + id = 'pig-gradle-plugin' + // version — inherited from root + // group — inherited from root + displayName = "PIG Gradle Plugin" + description = "The PIG gradle plugin exposes a Gradle task to generate sources from a PIG type universe" + implementationClass = 'org.partiql.pig.plugin.PigPlugin' + } + } +} + +publishing { + repositories { + maven { + name = 'mavenLocalPlugin' + url = '../maven-local-plugin' + } + } +} + +java { + sourceSets { + main.java.srcDirs = ["src"] + main.resources.srcDirs = ["resources"] + test.java.srcDirs = ["test"] + test.resources.srcDirs = ["test-resources"] + } +} + +plugins.withId('org.jetbrains.kotlin.jvm', { _ -> + + sourceSets { + main.kotlin.srcDirs = ["src"] + test.kotlin.srcDirs = ["test"] + } +}) diff --git a/pig-gradle-plugin/src/org/partiql/pig/plugin/PigExtension.kt b/pig-gradle-plugin/src/org/partiql/pig/plugin/PigExtension.kt new file mode 100644 index 0000000..d1406f0 --- /dev/null +++ b/pig-gradle-plugin/src/org/partiql/pig/plugin/PigExtension.kt @@ -0,0 +1,33 @@ +package org.partiql.pig.plugin + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import javax.inject.Inject + +abstract class PigExtension @Inject constructor(project: Project) { + + private val objects = project.objects + + val conventionalOutDir: String + + init { + conventionalOutDir = "${project.buildDir}/generated-sources/pig" + } + + // required + val target: Property = objects.property(String::class.java) + + // optional + val outputFile: Property = objects.property(String::class.java) + + // optional + val outputDir: Property = objects + .property(String::class.java) + .convention(conventionalOutDir) + + // optional + val namespace: Property = objects.property(String::class.java) + + // optional + val template: Property = objects.property(String::class.java) +} diff --git a/pig-gradle-plugin/src/org/partiql/pig/plugin/PigPlugin.kt b/pig-gradle-plugin/src/org/partiql/pig/plugin/PigPlugin.kt new file mode 100644 index 0000000..90af799 --- /dev/null +++ b/pig-gradle-plugin/src/org/partiql/pig/plugin/PigPlugin.kt @@ -0,0 +1,89 @@ +package org.partiql.pig.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.SourceDirectorySet +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer + +abstract class PigPlugin : Plugin { + + override fun apply(project: Project) { + // Ensure `sourceSets` extension exists + project.pluginManager.apply(JavaPlugin::class.java) + + // Adds pig source set extension to all source sets + project.sourceSets().forEach { sourceSet -> + val name = sourceSet.name + val sds = project.objects.sourceDirectorySet(name, "$name PIG source") + sds.srcDir("src/$name/pig") + sds.include("**/*.ion") + sourceSet.extensions.add("pig", sds) + } + + // Extensions for pig compiler arguments + val ext = project.extensions.create("pig", PigExtension::class.java, project) + + // Create tasks after source sets have been evaluated + project.afterEvaluate { + project.sourceSets().forEach { sourceSet -> + // Pig generate all for the given source set + val pigAllTaskName = getPigAllTaskName(sourceSet) + val pigAllTask = project.tasks.create(pigAllTaskName) { + it.group = "pig" + it.description = "Generate all PIG sources for ${sourceSet.name} source set" + } + + // If outDir is conventional, add generated sources to javac sources + // Else you're responsible for your own configuration choices + var outDir = ext.outputDir.get() + if (outDir == ext.conventionalOutDir) { + outDir = outDir + "/" + sourceSet.name + sourceSet.java.srcDir(outDir) + } + + // Create a pig task for each type universe and each source set + (sourceSet.extensions.getByName("pig") as SourceDirectorySet).files.forEach { file -> + val universeName = file.name.removeSuffix(".ion").lowerToCamelCase().capitalize() + val pigTask = project.tasks.create(pigAllTaskName + universeName, PigTask::class.java) { task -> + task.description = "Generated PIG sources for $universeName" + task.universe.set(file.absolutePath) + task.target.set(ext.target) + task.outputDir.set(outDir) + task.outputFile.set(ext.outputFile) + task.namespace.set(ext.namespace) + task.template.set(ext.template) + } + pigAllTask.dependsOn(pigTask) + } + + // Execute pig tasks before compiling + project.tasks.named(sourceSet.compileJavaTaskName) { + it.dependsOn(pigAllTask) + } + } + } + } + + private fun Project.sourceSets(): List = extensions.getByType(SourceSetContainer::class.java).toList() + + private fun getPigAllTaskName(sourceSet: SourceSet) = when (sourceSet.name) { + "main" -> "generatePigSource" + else -> "generatePig${sourceSet.name.capitalize()}Source" + } + + /** + * Type Universe files are lower hyphen, but Gradle tasks are lower camel + */ + private fun String.lowerToCamelCase(): String = + this.split('-') + .filter { it.isNotEmpty() } + .mapIndexed { i, str -> + when (i) { + 0 -> str + else -> str.capitalize() + } + } + .joinToString(separator = "") +} diff --git a/pig-gradle-plugin/src/org/partiql/pig/plugin/PigTask.kt b/pig-gradle-plugin/src/org/partiql/pig/plugin/PigTask.kt new file mode 100644 index 0000000..c593294 --- /dev/null +++ b/pig-gradle-plugin/src/org/partiql/pig/plugin/PigTask.kt @@ -0,0 +1,86 @@ +package org.partiql.pig.plugin + +import org.gradle.api.DefaultTask +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option + +abstract class PigTask : DefaultTask() { + + init { + group = "pig" + } + + @get:Input + @get:Option( + option = "universe", + description = "Type universe input file", + ) + abstract val universe: Property + + @get:Input + @get:Option( + option = "target", + description = "Target language", + ) + abstract val target: Property + + @get:Input + @get:Optional + @get:Option( + option = "outputFile", + description = "Generated output file (for targets that output a single file)", + ) + abstract val outputFile: Property + + @get:Input + @get:Optional + @get:Option( + option = "outputDir", + description = "Generated output directory (for targets that output multiple files)", + ) + abstract val outputDir: Property + + @get:Input + @get:Optional + @get:Option( + option = "namespace", + description = "Namespace for generated code", + ) + abstract val namespace: Property + + @get:Input + @get:Optional + @get:Option( + option = "template", + description = "Path to an Apache FreeMarker template", + ) + abstract val template: Property + + @TaskAction + fun action() { + val args = mutableListOf() + // required args + args += listOf("-u", universe.get()) + args += listOf("-t", target.get()) + // optional args + if (outputFile.isPresent) { + args += listOf("-o", outputFile.get()) + } + if (outputDir.isPresent) { + args += listOf("-d", outputDir.get()) + } + if (namespace.isPresent) { + args += listOf("-n", namespace.get()) + } + if (template.isPresent) { + args += listOf("-e", template.get()) + } + // invoke pig compiler, offloads all arg handling to the application + // also invoking via the public interface for consistency + println("pig ${args.joinToString(" ")}") + org.partiql.pig.main(args.toTypedArray()) + } +} diff --git a/pig-tests/build.gradle b/pig-tests/build.gradle index a918e6d..a03c9a8 100644 --- a/pig-tests/build.gradle +++ b/pig-tests/build.gradle @@ -1,5 +1,3 @@ -import java.nio.file.Paths - /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * @@ -23,6 +21,7 @@ plugins { // https://docs.gradle.org/5.0/userguide/publishing_maven.html#header id 'org.jetbrains.kotlin.jvm' id 'signing' + id 'pig-gradle-plugin' } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { @@ -31,6 +30,25 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } } +// remove after pig-example is created +def PIG_OUTPUT_DIR = file("./src/org/partiql/pig/tests/generated/").absolutePath + +pig { + target = "kotlin" + namespace = "org.partiql.pig.tests.generated" + // remove after pig-example is created + outputDir = PIG_OUTPUT_DIR +} + +// remove after https://github.com/partiql/partiql-ir-generator/pull/128 +sourceSets { + main { + pig { + srcDir "type-domains" + } + } +} + dependencies { // compile-time dependencies implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' @@ -43,53 +61,15 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.2' } -final PIG_INPUT_DIR = './type-domains' -final PIG_OUTPUT_DIR = './src/org/partiql/pig/tests/generated/' -final PIG_TARGET_PACKAGE = 'org.partiql.pig.tests.generated' -final universesToGenerate = ['toy-lang', 'sample-universe', 'partiql-basic'] - -// the universe-specific tasks registered below will be added as dependencies of this task -// provides a simplified way to ask pig to generate all the universes, i.e. ./gradlew pig_all -task pig_all { - group 'pig' -} - -// Make sure all pig tasks have completed before the kotlin code is compiled. -compileKotlin.dependsOn pig_all - -universesToGenerate.each({ u -> - tasks.register("pig-generate-${u}") { generateTask -> - group 'pig' - def pathToPig = new File(projectDir, '../pig/build/install/pig/bin/pig').getCanonicalPath().toString() - def pathToUniverseFile = Paths.get(projectDir.toString(), PIG_INPUT_DIR, "${u}.ion") - - tasks.findByName("pig_all").dependsOn generateTask - - // Make sure the :pig:installDist task has completed before the pig tasks are generated - // this installs the binary and gradle-generated launch script to the build directory - // so it is accessible to the `exec` task below. - dependsOn project(":pig").tasks.installDist - - doLast { - exec { - workingDir projectDir - commandLine pathToPig, '-u', pathToUniverseFile, '-t', 'kotlin', '-n', PIG_TARGET_PACKAGE, '-d', PIG_OUTPUT_DIR - } - } - } -}) - -// This task removes any *.generated.kt files left behind from previous builds runs. Without this, there is nothing to -// remove generated code for deleted and renamed PIG domains, which could be very confusing. -tasks.register('clean-pig-generated-files', Delete) { +// remove once pig-example is created +tasks.register('pigClean', Delete) { group 'pig' delete fileTree(PIG_OUTPUT_DIR).matching { include "*.generated.kt" } } -pig_all.shouldRunAfter('clean-pig-generated-files') - +// remove once pig-example is created ktlint { filter { exclude("**/*.generated.kt") diff --git a/settings.gradle b/settings.gradle index e646e1f..9952783 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,12 @@ * permissions and limitations under the License. */ +pluginManagement { + includeBuild("pig-gradle-plugin") + repositories { + gradlePluginPortal() + } +} rootProject.name = 'PIG'