diff --git a/README.md b/README.md index 19f886b..e29524b 100644 --- a/README.md +++ b/README.md @@ -25,20 +25,33 @@ Both are available in [Maven Central](https://search.maven.org/search?q=partiql- ### Gradle -There are [plans to make a Gradle plugin for PIG](https://github.com/partiql/partiql-ir-generator/issues/102) but one -has not been completed yet. - -Without the aforementioned plugin, the best way to use pig with gradle is: - -- Add a dependency on PIG in your project's `buildSrc/build.gradle` file. This will make the API of PIG available to all - other `build.gradle` files in your project. - ([Example](https://github.com/partiql/partiql-lang-kotlin/blob/main/buildSrc/build.gradle#L9)) -- Add a dependency on PIG's runtime library in your project. - ([Example](https://github.com/partiql/partiql-lang-kotlin/blob/28701e23cf3bd397a67e8d9ab4f68feff953aea1/lang/build.gradle#L48)) -- Add a custom task that uses PIG's internal - APIs. ([Example](https://github.com/partiql/partiql-lang-kotlin/blob/51e7da7b5e63e45f01c4df101168b2117a17a2d1/lang/build.gradle#L64-L96)) -- Make sure your custom task executes *before* the `compileKotlin` task. - ([Example](https://github.com/partiql/partiql-lang-kotlin/blob/28701e23cf3bd397a67e8d9ab4f68feff953aea1/lang/build.gradle#L89)) +```groovy +plugins { + id 'pig-gradle-plugin' +} + +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' + } + } +} + +pig { + target = 'kotlin' // required + namespace = ... // optional + template = ... // optional + outDir = 'build/generated-sources/pig/' // default +} +``` ### Other Build Systems diff --git a/pig-gradle-plugin/build.gradle.kts b/pig-gradle-plugin/build.gradle.kts new file mode 100644 index 0000000..f50aee2 --- /dev/null +++ b/pig-gradle-plugin/build.gradle.kts @@ -0,0 +1,55 @@ +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() +} + +version = "0.5.1-SNAPSHOT" +group = "org.partiql" + +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") +} + +tasks.test { + useJUnitPlatform() +} + +pluginBundle { + website = "https://github.com/partiql/partiql-ir-generator/wiki" + vcsUrl = "https://github.com/partiql/partiql-ir-generator" + tags = listOf("partiql", "pig", "ir", "partiql-ir-generator") +} + +gradlePlugin { + plugins { + create("pig-gradle-plugin") { + id = "pig-gradle-plugin" + 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" + } + } +} + +// +// // TODO https://github.com/partiql/partiql-ir-generator/issues/132 +// publishing { +// repositories { +// maven { +// name = 'mavenLocalPlugin' +// url = '../maven-local-plugin' +// } +// } +// } diff --git a/pig-gradle-plugin/src/main/kotlin/org/partiql/pig/plugin/PigExtension.kt b/pig-gradle-plugin/src/main/kotlin/org/partiql/pig/plugin/PigExtension.kt new file mode 100644 index 0000000..d1406f0 --- /dev/null +++ b/pig-gradle-plugin/src/main/kotlin/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/main/kotlin/org/partiql/pig/plugin/PigPlugin.kt b/pig-gradle-plugin/src/main/kotlin/org/partiql/pig/plugin/PigPlugin.kt new file mode 100644 index 0000000..90af799 --- /dev/null +++ b/pig-gradle-plugin/src/main/kotlin/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/main/kotlin/org/partiql/pig/plugin/PigTask.kt b/pig-gradle-plugin/src/main/kotlin/org/partiql/pig/plugin/PigTask.kt new file mode 100644 index 0000000..0e8cff5 --- /dev/null +++ b/pig-gradle-plugin/src/main/kotlin/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.kts b/pig-tests/build.gradle.kts index dfd76f0..07d0946 100644 --- a/pig-tests/build.gradle.kts +++ b/pig-tests/build.gradle.kts @@ -13,10 +13,9 @@ * permissions and limitations under the License. */ -import java.nio.file.Paths - plugins { id("pig.conventions") + id("pig-gradle-plugin") id("java-library") } @@ -24,63 +23,25 @@ dependencies { implementation(project(":pig-runtime")) } -tasks { - - val pigOutputDir = "src/main/kotlin/org/partiql/pig/tests/generated/" - val pigOutputPackage = "org.partiql.pig.tests.generated" - val pigInputDir = "src/test/pig/" - val universes = listOf("toy-lang", "sample-universe", "partiql-basic") - - val pigClean = register("pig-clean", Delete::class) { - group = "pig" - description = "deletes all pig generated files" - delete( - fileTree(pigOutputDir).matching { - include("*.generated.kt") - } - ) - } +// remove after pig-example is created +val pigOutputDir = file("./src/main/kotlin/org/partiql/pig/tests/generated/").absolutePath - val pigAll = register("pig-all") { - group = "pig" - description = "run all pig generation tasks" - shouldRunAfter(pigClean) - } - - named("compileKotlin") { - dependsOn(pigAll) - } - - // :pig:installDist creates the pig jar launch script required for `exec` - val installDist = project(":pig").tasks.named("installDist") - - universes.forEach { u -> - val t = register("pig-generate-$u") { - group = "pig" - description = "pig generation for type universe $u" - dependsOn(installDist) - - // pig script from :pig:installDist - val pathToPig = File(projectDir, "../pig/build/install/pig/bin/pig").canonicalPath - val pathToUniverse = Paths.get(projectDir.toString(), pigInputDir, "$u.ion") +pig { + target.set("kotlin") + namespace.set("org.partiql.pig.tests.generated") + // remove after pig-example is created + outputDir.set(pigOutputDir) +} - doLast { - exec { - workingDir = projectDir - commandLine( - pathToPig, - "-u", pathToUniverse, - "-t", "kotlin", - "-n", pigOutputPackage, - "-d", pigOutputDir, - ) - } - } - } - pigAll { - dependsOn(t) +// remove after pig-example is created +tasks.register("pigClean", Delete::class) { + group = "pig" + description = "deletes all pig generated files" + delete( + fileTree(pigOutputDir).matching { + include("*.generated.kt") } - } + ) } configure { diff --git a/settings.gradle.kts b/settings.gradle.kts index 59bfbc9..5e9117b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,8 +15,15 @@ rootProject.name = "PIG" +pluginManagement { + includeBuild("pig-gradle-plugin") + repositories { + gradlePluginPortal() + } +} + include( "pig", "pig-runtime", - "pig-tests", + "pig-tests" )