diff --git a/README.md b/README.md index 6fec4b8..639acfd 100644 --- a/README.md +++ b/README.md @@ -50,22 +50,12 @@ plugins { // Settings here apply on a per-project basis. See below for available settings; all properties // are optional, and you don't need to include this block at all if you are fine with defaults. elide { - // Use Elide's Maven resolver and downloader instead of Gradle's. Defaults to `true` when an - // `elide.pkl` file is present in the project root. - enableInstall = true - - // Use Elide to compile Java instead of the stock Compiler API facilities used by Gradle. - // Defaults to `true` if the plugin is active in the project at all. - enableJavaCompiler = true - - // Enable Elide project awareness for Gradle. For example, build scripts can show up as runnable - // exec tasks within the Gradle build. - enableProjectIntegration = true - - // Set the path to the project manifest, expressed in Pkl format. Elide project manifests can - // specify dependencies, build scripts, and other project metadata. Defaults to `elide.pkl` and - // automatically finds any present `elide.pkl` in the active project. - manifest = layout.projectDirectory.file("elide.pkl") + // specifies to use only locally installed Elide + binary.useLocalOnly() + // specifies to use a local version if version is the same + binary.useLocalIfApplicable(version = "1.0.0-beta6") + // or + binary.useProjectOnly(version = null) } ``` diff --git a/elide-gradle-catalog/build.gradle.kts b/elide-gradle-catalog/build.gradle.kts deleted file mode 100644 index d7f1f76..0000000 --- a/elide-gradle-catalog/build.gradle.kts +++ /dev/null @@ -1,58 +0,0 @@ -plugins { - `version-catalog` - `maven-publish` - signing -} - -val latestElide = findProperty("elide.version")?.toString() ?: error( - "Please provide the 'elide.version' property in the gradle.properties file or as a command line argument." -) - -group = "dev.elide.gradle" -version = findProperty("version")?.toString() ?: error( - "Please provide the 'version' property in the gradle.properties file." -) - -val mainPluginId = "dev.elide" - -val allLibs = listOf( - "core", - "base", - "graalvm", -) - -catalog { - versionCatalog { - version("elide", latestElide) - plugin("elide", mainPluginId).versionRef("elide") - allLibs.forEach { - library(it, "dev.elide", "elide-$it").versionRef("elide") - } - } -} - -publishing { - repositories { - maven { - url = uri(rootProject.layout.buildDirectory.dir("elide-maven")) - } - } - - publications { - create("maven") { - from(components["versionCatalog"]) - - pom { - name = "Elide Gradle Catalog" - description = "Provides mapped versions for Elide and related libraries and plugins." - inceptionYear = "2023" - url = "https://elide.dev" - } - } - } -} - -signing { - useGpgCmd() - sign(publishing.publications["maven"]) -} diff --git a/elide-gradle-plugin/build.gradle.kts b/elide-gradle-plugin/build.gradle.kts index bcc42c7..ab0440f 100644 --- a/elide-gradle-plugin/build.gradle.kts +++ b/elide-gradle-plugin/build.gradle.kts @@ -1,17 +1,12 @@ -import de.undercouch.gradle.tasks.download.Download - plugins { `maven-publish` + `kotlin-dsl` `java-gradle-plugin` signing - id("com.gradle.plugin-publish") version "1.2.1" - id("de.undercouch.download") version "5.6.0" + alias(libs.plugins.gradle.publish) + alias(libs.plugins.undercouch.download) } -val elideVersion = findProperty("elide.version")?.toString() ?: error( - "Please provide the 'elide.version' property in the gradle.properties file or as a command line argument." -) - group = "dev.elide.gradle" version = findProperty("version")?.toString() ?: error( "Please provide the 'version' property in the gradle.properties file." @@ -25,94 +20,46 @@ publishing { } } -val elideArch = when (System.getProperty("os.arch").lowercase()) { - "x86_64", "amd64" -> "amd64" - "arm64", "aarch64" -> "arm64" - else -> error("Unsupported architecture: ${System.getProperty("os.arch")}") -} -val elidePlatform = when (System.getProperty("os.name").lowercase()) { - "linux" -> "linux-$elideArch" - "mac os x" -> "darwin-$elideArch" - "windows" -> "windows-$elideArch" - else -> error("Unsupported OS: ${System.getProperty("os.name")}") -} - repositories { mavenCentral() } -val elideRuntime: Configuration by configurations.creating { - isCanBeResolved = true -} - dependencies { - elideRuntime(files(zipTree(rootProject.layout.buildDirectory.dir("elide-runtime")))) + implementation(libs.pluginClasspath.undercouch.download) + implementation(libs.sigstore) - // Use JUnit test framework for unit tests - testImplementation("junit:junit:4.13.1") + testImplementation(gradleTestKit()) + testImplementation(libs.kotlin.test.junit5) } -gradlePlugin { - website = "https://elide.dev" - vcsUrl = "https://github.com/elide-dev/gradle" - - val elide by plugins.creating { - id = "dev.elide" - displayName = "Elide Gradle Plugin" - implementationClass = "dev.elide.gradle.ElideGradlePlugin" - description = "Use the Elide runtime and build tools from Gradle" - tags.set(listOf("elide", "graalvm", "java", "javac", "maven", "dependencies", "resolver")) - } +kotlin { + explicitApi() } -// Add a source set and a task for a functional test suite -val functionalTest: SourceSet by sourceSets.creating -gradlePlugin.testSourceSets(functionalTest) - -configurations[functionalTest.implementationConfigurationName].extendsFrom(configurations.testImplementation.get()) - -val runtimeHome = layout.buildDirectory.dir("elide-runtime/elide-$elideVersion-$elidePlatform") +val functionalTest: SourceSet by sourceSets.creating { + configurations[implementationConfigurationName].extendsFrom(configurations.testImplementation.get()) +} -val functionalTestTask = tasks.register("functionalTest") { +val functionalTestTask by tasks.register("functionalTest") { testClassesDirs = functionalTest.output.classesDirs classpath = configurations[functionalTest.runtimeClasspathConfigurationName] + functionalTest.output } -val downloadElide by tasks.registering(Download::class) { - src("https://elide.zip/cli/v1/snapshot/$elidePlatform/$elideVersion/elide.tgz") - dest(layout.buildDirectory.dir("elide-runtime")) - outputs.file(layout.buildDirectory.file("elide-runtime/elide.tgz")) -} - -val extractElide by tasks.registering(Copy::class) { - from(tarTree(layout.buildDirectory.file("elide-runtime/elide.tgz"))) - into(layout.buildDirectory.dir("elide-runtime")) - inputs.file(layout.buildDirectory.file("elide-runtime/elide.tgz")) - dependsOn(downloadElide) -} - -val prepareElide by tasks.registering { - group = "build" - description = "Prepare the Elide runtime" - dependsOn(downloadElide, extractElide) -} +gradlePlugin { + website = "https://elide.dev" + vcsUrl = "https://github.com/elide-dev/gradle" -val checkElide by tasks.registering(Exec::class) { - executable = runtimeHome.get().file("elide").asFile.absolutePath - args("--version") - dependsOn(downloadElide, extractElide, prepareElide) -} + testSourceSets(sourceSets.test.get(), functionalTest) -listOf( - tasks.build, - tasks.test, - tasks.check, -).forEach { - it.configure { - dependsOn(downloadElide, extractElide, prepareElide, checkElide) + plugins.register("elide") { + id = "dev.elide" + displayName = "Elide Gradle Plugin" + implementationClass = "dev.elide.gradle.ElideGradlePlugin" + description = "Use the Elide runtime and build tools from Gradle" + tags.set(listOf("elide", "graalvm", "java", "javac", "kotlin", "kotlinc", "javadoc", "maven", "dependencies", "resolver")) } } tasks.check { dependsOn(functionalTestTask) -} +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/functionalTest/java/com/example/plugin/ElidePluginFunctionalTest.java b/elide-gradle-plugin/src/functionalTest/java/com/example/plugin/ElidePluginFunctionalTest.java deleted file mode 100644 index 5aec2a0..0000000 --- a/elide-gradle-plugin/src/functionalTest/java/com/example/plugin/ElidePluginFunctionalTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.example.plugin; - -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.GradleRunner; -import org.junit.Test; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.nio.file.Files; -import java.nio.file.Paths; - -import static org.junit.Assert.assertTrue; - -public class ElidePluginFunctionalTest { - @Test - public void canRunTasks() throws IOException { - File projectDir = new File("build/functionalTest"); - Files.createDirectories(projectDir.toPath()); - writeString(new File(projectDir, "settings.gradle"), ""); - writeString(new File(projectDir, "build.gradle"), - """ - plugins { - id('dev.elide') - } - """); - - GradleRunner.create() - .forwardOutput() - .withPluginClasspath() - .withArguments("tasks") - .withProjectDir(projectDir) - .build(); - } - - @Test - public void canShimJavac() throws IOException { - File projectDir = new File("build/functionalTestJavac"); - var helloPathRelative = Paths.get("src/main/java/com/example/HelloWorld.java"); - var helloPath = projectDir.toPath().resolve(helloPathRelative); - Files.createDirectories(projectDir.toPath()); - writeString(new File(projectDir, "settings.gradle.kts"), ""); - writeString(new File(projectDir, "build.gradle.kts"), - """ - plugins { - id("dev.elide") - java - } - repositories { - mavenCentral() - } - """); - - Files.createDirectories(helloPath.getParent()); - writeString(new File(projectDir, "src/main/java/com/example/HelloWorld.java"), - """ - package com.example; - - public class HelloWorld { - public static void main(String[] args) { - System.out.println("Hello, World!"); - } - } - """); - - // Run `tasks` (tests configuration phase) - BuildResult result = GradleRunner.create() - .forwardOutput() - .withPluginClasspath() - .withArguments("tasks") - .withProjectDir(projectDir) - .build(); - - assertTrue(result.getOutput().contains("BUILD SUCCESSFUL")); - - BuildResult buildResult = GradleRunner.create() - .forwardOutput() - .withPluginClasspath() - .withArguments("build", "--info") - .withProjectDir(projectDir) - .build(); - - assertTrue(buildResult.getOutput().contains("BUILD SUCCESSFUL")); - assertTrue(buildResult.getOutput().contains("Using Elide ")); - } - - private void writeString(File file, String string) throws IOException { - try (Writer writer = new FileWriter(file)) { - writer.write(string); - } - } -} diff --git a/elide-gradle-plugin/src/functionalTest/kotlin/dev/elide/gradle/plugin/test/ElidePluginFunctionalTest.kt b/elide-gradle-plugin/src/functionalTest/kotlin/dev/elide/gradle/plugin/test/ElidePluginFunctionalTest.kt new file mode 100644 index 0000000..941f20d --- /dev/null +++ b/elide-gradle-plugin/src/functionalTest/kotlin/dev/elide/gradle/plugin/test/ElidePluginFunctionalTest.kt @@ -0,0 +1,186 @@ +package dev.elide.gradle.plugin.test + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.createFile +import kotlin.io.path.exists +import kotlin.io.path.writeText + +class ElidePluginFunctionalTest { + private companion object { + const val ELIDE_VERSION = "1.0.0-Beta7" + } + + @field:TempDir + private lateinit var elideInstallDir: Path + + @Test + fun `check whether plugin successfully applies`(@TempDir projectDir: Path) { + // Given: a minimal project with the Elide plugin applied + val settingsFile = projectDir.resolve("settings.gradle.kts") + settingsFile.createFile() + + val buildFile = projectDir.resolve("build.gradle.kts") + buildFile.writeText( + """ + plugins { + id("dev.elide") + } + """.trimIndent() + ) + + // When: we run the 'tasks' task + val result = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withPluginClasspath() + .withArguments("tasks") + .forwardOutput() + .build() + + // Then: the build should succeed + assert(result.output.contains("BUILD SUCCESSFUL")) + } + + + @Test + fun `check elide download-related task with build caching`(@TempDir projectDir: Path) { + // Given: + projectDir.resolve("settings.gradle.kts").createFile() + + val buildFile = projectDir.resolve("build.gradle.kts") + buildFile.writeText( + """ + import dev.elide.gradle.configuration.binary.* + + plugins { + id("dev.elide") + } + + elide.settings { + binary { + version = "$ELIDE_VERSION" + useProjectBinary(ElideBinaryResolutionSource.Project("${elideInstallDir.absolutePathString()}")) + } + } + """.trimIndent() + ) + + // When: we run the prepareDownloadedElideCli task twice with build cache enabled + val firstResult = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withPluginClasspath() + .withArguments("prepareDownloadedElideCli", "--build-cache") + .forwardOutput() + .build() + + val secondResult = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withPluginClasspath() + .withArguments("prepareDownloadedElideCli", "--build-cache") + .forwardOutput() + .build() + + // Then: the first build should succeed and run the task + val firstTask = firstResult.task(":prepareDownloadedElideCli") + assertEquals( + TaskOutcome.SUCCESS, + firstTask?.outcome, + ) + assert(firstResult.output.contains("BUILD SUCCESSFUL")) + assert(elideInstallDir.resolve("elide").exists()) { "Expected elide binary to be present" } + + // Then: the second build should be FROM-CACHE or UP-TO-DATE + val secondTask = secondResult.task(":prepareDownloadedElideCli") + assert(secondTask?.outcome in listOf(TaskOutcome.FROM_CACHE, TaskOutcome.UP_TO_DATE, TaskOutcome.SKIPPED)) { + "Expected second run to be cached or skipped" + } + assert(elideInstallDir.resolve("elide").exists()) { "Expected elide binary to be present" } + assert(secondResult.output.contains("BUILD SUCCESSFUL")) + } + + @Test + fun `test elide install with auto-generated helper file`(@TempDir projectDir: Path) { + // Given: a minimal project with one dependency and repository + projectDir.resolve("settings.gradle.kts").createFile() + + projectDir.resolve("project.pkl").createFile().writeText( + """ + amends "elide:project.pkl" + + import "build/elide-runtime/generated/module.pkl" as gradle + + dependencies { + maven { + packages = gradle.packages + testPackages = gradle.testPackages + repositories = gradle.repositories + } + } + """.trimIndent() + ) + + val buildFile = projectDir.resolve("build.gradle.kts") + buildFile.writeText( + """ + import dev.elide.gradle.configuration.binary.* + + plugins { + id("dev.elide") + java + } + + elide.settings { + binary { + version = "$ELIDE_VERSION" + useProjectBinary(ElideBinaryResolutionSource.Project("${elideInstallDir.absolutePathString()}")) + } + + features { + generatePklBuildConfiguration = true + } + } + + dependencies { + implementation("com.google.guava:guava:33.4.8-jre") + } + """.trimIndent() + ) + + // When: + val firstResult = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withPluginClasspath() + .withArguments(":elideInstall", "--build-cache", "--configuration-cache") + .forwardOutput() + .build() + + val secondResult = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withPluginClasspath() + .withArguments(":elideInstall", "--build-cache --configuration-cache") + .forwardOutput() + .build() + + // Then: the first build should succeed and run the task + val firstTask = firstResult.task(":prepareDownloadedElideCli") + assertEquals( + TaskOutcome.SUCCESS, + firstTask?.outcome, + ) + assert(firstResult.output.contains("BUILD SUCCESSFUL")) + assert(elideInstallDir.resolve("elide").exists()) { "Expected elide binary to be present" } + + // Then: the second build should be FROM-CACHE or UP-TO-DATE + val secondTask = secondResult.task(":prepareDownloadedElideCli") + assert(secondTask?.outcome in listOf(TaskOutcome.FROM_CACHE, TaskOutcome.UP_TO_DATE, TaskOutcome.SKIPPED)) { + "Expected second run to be cached or skipped" + } + assert(elideInstallDir.resolve("elide").exists()) { "Expected elide binary to be present" } + assert(secondResult.output.contains("BUILD SUCCESSFUL")) + } +} diff --git a/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideExtension.java b/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideExtension.java deleted file mode 100644 index d05b5cc..0000000 --- a/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideExtension.java +++ /dev/null @@ -1,128 +0,0 @@ -package dev.elide.gradle; - -import org.gradle.api.Project; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.file.RegularFile; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; - -import java.nio.file.Path; - -public class ElideExtension implements ElideExtensionConfig { - private static final boolean USE_ROOT_FOR_DEPS = true; - private static final String DEFAULT_DEV_ROOT = ".dev"; - protected Project activeProject; - protected boolean enableInstall = false; - protected boolean useBuildEmbedded = false; - protected boolean enableJavacIntegration = true; - protected boolean enableProjectIntegration = true; - protected boolean enableMavenIntegration = true; - protected boolean enableShim = true; - protected Property doEnableInstall; - protected Property doEmbeddedBuild; - protected Property doUseMavenIntegration; - protected Property doEnableProjects; - protected Property doEnableJavaCompiler; - protected Property doResolveElideFromPath; - protected Property enableDebugMode; - protected Property enableVerboseMode; - @PathSensitive(PathSensitivity.RELATIVE) protected RegularFileProperty projectManifest; - @PathSensitive(PathSensitivity.ABSOLUTE) protected RegularFileProperty activeElideBin; - @PathSensitive(PathSensitivity.RELATIVE) protected DirectoryProperty activeDevRoot; - @PathSensitive(PathSensitivity.RELATIVE) @Input protected RegularFileProperty activeLockfile; - - @Override - public Property getEnableInstall() { - return doEnableInstall; - } - - @Override - public Property getEnableEmbeddedBuild() { - return doEmbeddedBuild; - } - - @Override - public Property getEnableMavenIntegration() { - return doUseMavenIntegration; - } - - @Override - public Property getEnableJavaCompiler() { - return doEnableJavaCompiler; - } - - @Override - public Property getEnableProjectIntegration() { - return doEnableProjects; - } - - @Override - public RegularFileProperty getManifest() { - return projectManifest; - } - - @Override - public Property getResolveElideFromPath() { - return doResolveElideFromPath; - } - - @Override - public DirectoryProperty getDevRoot() { - return activeDevRoot; - } - - @Override - public RegularFileProperty getElideBin() { - return activeElideBin; - } - - @Override - public Property getDebug() { - return enableDebugMode; - } - - @Override - public Property getVerbose() { - return enableVerboseMode; - } - - boolean enableShim() { - return enableShim; - } - - Path resolveLocalDepsPath() { - return activeDevRoot.getAsFile().get().toPath() - .resolve("dependencies") - .resolve("m2"); - } - - Provider resolveLockfilePath() { - return activeDevRoot.file("elide.lock.bin"); - } - - ElideExtension(Project project, ObjectFactory objects) { - this.activeProject = project; - this.doEnableInstall = objects.property(Boolean.class).convention(enableInstall); - this.doEmbeddedBuild = objects.property(Boolean.class).convention(useBuildEmbedded); - this.doUseMavenIntegration = objects.property(Boolean.class).convention(enableMavenIntegration); - this.doEnableProjects = objects.property(Boolean.class).convention(enableProjectIntegration); - this.doEnableJavaCompiler = objects.property(Boolean.class).convention(enableJavacIntegration); - this.doResolveElideFromPath = objects.property(Boolean.class).convention(false); - this.projectManifest = objects.fileProperty() - .convention(project.getLayout().getProjectDirectory().file("elide.pkl")); - - var devRootProject = (USE_ROOT_FOR_DEPS ? activeProject.getRootProject() : activeProject); - this.activeElideBin = objects.fileProperty(); - this.activeDevRoot = objects.directoryProperty() - .convention(devRootProject.getLayout().getProjectDirectory().dir(DEFAULT_DEV_ROOT)); - - this.enableDebugMode = objects.property(Boolean.class).convention(false); - this.enableVerboseMode = objects.property(Boolean.class).convention(false); - this.activeLockfile = objects.fileProperty().convention(resolveLockfilePath()); - } -} diff --git a/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideExtensionConfig.java b/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideExtensionConfig.java deleted file mode 100644 index f0503f2..0000000 --- a/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideExtensionConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.elide.gradle; - -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; - -public interface ElideExtensionConfig { - Property getEnableInstall(); - Property getEnableEmbeddedBuild(); - Property getEnableMavenIntegration(); - Property getEnableJavaCompiler(); - Property getEnableProjectIntegration(); - RegularFileProperty getManifest(); - RegularFileProperty getElideBin(); - Property getResolveElideFromPath(); - Property getDebug(); - Property getVerbose(); - DirectoryProperty getDevRoot(); -} diff --git a/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideGradlePlugin.java b/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideGradlePlugin.java deleted file mode 100644 index c084a58..0000000 --- a/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideGradlePlugin.java +++ /dev/null @@ -1,316 +0,0 @@ -package dev.elide.gradle; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.tasks.compile.JavaCompile; - -import javax.inject.Inject; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.stream.Collectors; - -public class ElideGradlePlugin implements Plugin { - // Plugin ID for Gradle's built-in Java support. - private static final String javaPluginId = "java"; - - // Binary name for Elide. - private static final String elideBinName = "elide"; - - // Extension name for configuring Elide's Gradle plugin. - private static final String elideExtensionName = elideBinName; - - // Project where the plugin is installed. - private final Project activeProject; - - @Inject - public ElideGradlePlugin(Project project) { - this.activeProject = project; - } - - // Configure a Java compile task to use Elide instead of the standard compiler API. - private Task configureJavaCompileToUseElide(Path elide, Project project, JavaCompile task, ElideExtension ext) { - project.getLogger().info( - "Installing Elide's javac support for task '{}' within project '{}'", - task.getName(), - activeProject.getName()); - - Path javaHome = Paths.get(System.getProperty("java.home")); - Path resolvedElide = null; - if (ext.enableShim()) { - var javaHomeShim = javaHome - .resolve("bin") - .resolve("elide-javac"); - - if (!Files.exists(javaHomeShim)) { - if (Files.isWritable(javaHomeShim.getParent())) { - // we can create the shim; if we are configured to do, we should do so now. - try(var writer = Files.newBufferedWriter(javaHomeShim)) { - // write the shim to the file - writer.write("#!/bin/sh\n"); - writer.write("exec " + resolvePathToElide().toAbsolutePath() + " javac -- \"$@\"\n"); - } catch (IOException e) { - throw new RuntimeException("Failed to write Elide javac shim", e); - } - } else { - // we can't write the shim, and it's not there, and we need it, so we should warn and fall back. - project.getLogger().warn("Elide's javac shim was not found at '{}'; falling back to stock javac.", - javaHomeShim.toAbsolutePath()); - return task; - } - } else if (!Files.isExecutable(javaHomeShim)) { - // the shim is there, but it's not executable. - var result = javaHomeShim.toFile().setExecutable(true); - if (result) { - // we're good to go - resolvedElide = javaHomeShim; - } else { - // we can't write the shim, and it's not there, and we need it, so we should warn and fall back. - project.getLogger().warn("Elide's javac shim isn't executable, and can't be made executable Please " + - "run 'chmod +x {}' to fix this.", - javaHomeShim.toAbsolutePath()); - return task; - } - } else { - // the shim is there and executable, so we can use it. - resolvedElide = javaHomeShim; - } - } else { - // if the shim is not enabled, we use the Elide binary directly. - resolvedElide = elide; - } - if (resolvedElide == null) { - project.getLogger().error("Failed to resolve Elide javac shim, and Java Home is not writable."); - throw new RuntimeException("Failed to resolve Elide javac shim; is your Java Home writable?"); - } - - Objects.requireNonNull(resolvedElide); - var pathAsString = resolvedElide.toString(); - var options = task.getOptions(); - var forkOptions = options.getForkOptions(); - options.setFork(true); - forkOptions.setExecutable(pathAsString); - - // if the shim is not enabled, we need to pass the `javac` flag and the separator (`--`) so that the binary can - // resolve the arguments correctly. - if (!ext.enableShim()) { - forkOptions.setJavaHome(javaHome.toFile()); - var allArgs = forkOptions.getJvmArgs(); - if (allArgs == null) { - allArgs = Collections.emptyList(); - } - var prefixed = new ArrayList(allArgs.size() + 2); - prefixed.add("javac"); - prefixed.add("--"); - prefixed.addAll(allArgs); - forkOptions.setJvmArgs(prefixed); - } - return task; - } - - // Install integration with Gradle's Java plugin; this prefers Elide's Java Compiler support. - private Collection installJavacSupport(Path path, Project project) { - var elideExtension = project.getExtensions().getByType(ElideExtension.class); - var compileTasks = project.getTasks().withType(JavaCompile.class); - project.getLogger().info( - "Installing Elide's javac support for {} Gradle tasks (path: '{}')", - compileTasks.size(), - path.toString()); - - return compileTasks.stream() - .map(compileTask -> configureJavaCompileToUseElide(path, project, compileTask, elideExtension)) - .collect(Collectors.toList()); - } - - // Install integration with Gradle's Maven root support. - private Collection installMavenDepsSupport(Project project, ElideExtension ext, boolean generateManifest) { - var repos = project.getRepositories(); - var localDepsPath = ext.resolveLocalDepsPath(); - repos.mavenLocal(it -> { - it.setName("elide"); - it.setUrl(URI.create("file://" + localDepsPath.toAbsolutePath())); - }); - return Collections.emptyList(); - } - - // Resolve the path to use when invoking the Elide binary. - private Path resolvePathToElide() { - var path = System.getenv("PATH"); - var pathSplit = path.split(File.pathSeparator); - var elideViaPath = Arrays.stream(pathSplit) - .map(Paths::get) - .map(p -> p.resolve(elideBinName)) - .filter(Files::exists) - .filter(Files::isExecutable) - .findFirst(); - - // prefer elide on the user's path - if (elideViaPath.isPresent()) { - return elideViaPath.get().toAbsolutePath(); - } - - // try the user's home? - var elideWithinHome = Paths.get(System.getProperty("user.home")) - .resolve("elide") - .resolve(elideBinName); - if (Files.exists(elideWithinHome) && Files.isExecutable(elideWithinHome)) { - return elideWithinHome.toAbsolutePath(); - } - throw new RuntimeException("Failed to find `elide` on your PATH; is it installed?"); - - // otherwise, we should resolve from the root project's layout. the plugin will download elide and install it - // in `/build/elide-runtime`. - // - // var elideBuildRoot = activeProject.getRootProject().getLayout().getBuildDirectory().dir("elide-runtime"); - // return elideBuildRoot.get().dir("bin").file(elideBinName).getAsFile().toPath(); - } - - // Determine whether Elide's Maven installer integration is enabled. - private boolean enableMavenInstaller(Project project, ElideExtension ext) { - var disableInstallerProp = project.findProperty("elide.builder.maven.install.enable"); - if (disableInstallerProp != null) { - return Boolean.parseBoolean(disableInstallerProp.toString()); - } - return ext.getEnableInstall().get() && ext.getEnableMavenIntegration().get(); - } - - // Detect any extant or configured project manifest file. If one is present, `elide install` is run unconditionally. - private boolean detectProjectManifest(Project project, ElideExtension ext) { - return ( - (ext.getManifest().isPresent() && ext.getManifest().getAsFile().get().exists()) || - project.getLayout().getProjectDirectory().file("elide.pkl").getAsFile().exists() - ); - } - - // Determine whether Elide's javac shim is enabled. - private boolean enableJavacShim(Project project, ElideExtension ext) { - var disableShimProp = project.findProperty("elide.builder.javac.enable"); - if (disableShimProp != null) { - return Boolean.parseBoolean(disableShimProp.toString()); - } - return ext.getEnableJavaCompiler().get(); - } - - // Call Elide in a subprocess at the provided path, and with the provided args; capture output and return it as a - // string to the caller. - private String callElideCaptured(Path path, String[] args) { - var allArgs = new String[args.length + 1]; - allArgs[0] = path.toAbsolutePath().toString(); - var i = 1; - for (var arg : args) { - allArgs[i] = arg; - i += 1; - } - - var cwd = activeProject.getLayout().getProjectDirectory().getAsFile(); - var subproc = new ProcessBuilder().command(allArgs).directory(cwd); - try { - var proc = subproc.start(); - var builder = new StringBuilder(); - - try (BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { - String line; - while ((line = br.readLine()) != null) { - builder.append(line).append(System.lineSeparator()); - } - } - try (BufferedReader br = new BufferedReader(new InputStreamReader(proc.getErrorStream()))) { - String line; - while ((line = br.readLine()) != null) { - builder.append(line).append(System.lineSeparator()); - } - } - var exit = proc.waitFor(); - if (exit != 0) { - // print output - activeProject.getLogger().error("Elide process exited with code {}: {}", exit, builder); - throw new RuntimeException("Elide failed with exit code " + exit); - } - return builder.toString().trim(); - } catch (InterruptedException ixr) { - throw new RuntimeException("Failed to wait for Elide process", ixr); - } catch (IOException ioe) { - throw new RuntimeException("Failed to start Elide captured process", ioe); - } - } - - @SuppressWarnings({"deprecation", "UnstableApiUsage"}) - public void apply(Project project) { - var elideResolved = resolvePathToElide(); - project.getLogger().debug("Elide resolved to '{}'", elideResolved); - if (!project.getGradle().getStartParameter().isConfigurationCacheRequested()) { - var versionPrinted = callElideCaptured(elideResolved, new String[]{"--version"}); - var version = versionPrinted.replace("\n", ""); - project.getLogger().lifecycle("Using Elide " + version); - } - - var objectUtil = project.getObjects(); - var extension = new ElideExtension(project, objectUtil); - project.getExtensions().add(elideExtensionName, extension); - - project.afterEvaluate(_ -> { - var pluginManager = project.getPluginManager(); - var javaPluginActive = pluginManager.hasPlugin(javaPluginId); - var javacSupportActive = enableJavacShim(project, extension); - var mavenInstallerActive = enableMavenInstaller(project, extension); - var hasProjectManifest = detectProjectManifest(project, extension); - - project.getLogger().info( - "Elide Java support: (pluginActive={}, javacSupport={})", - javaPluginActive, - javacSupportActive); - - Collection javacTasks = null; - if (javaPluginActive && javacSupportActive) { - javacTasks = installJavacSupport(elideResolved, project); - } - - boolean mustGenerateManifest = false; // @TODO implement - boolean installerEnabled = extension.getEnableInstall().get(); - boolean shouldRunInstallWithOrWithoutMaven = installerEnabled; - LinkedList allPrepTasks = new LinkedList<>(); - - if (mavenInstallerActive) { - // to enable integration with Maven dependency installation, we need to inject a local dependency root - // path, and we need to run `elide install` before compilation runs. dependencies must also be gathered - // if there is no project manifest file to read from. this comes first so that Gradle is configured to - // be aware of our Maven dependencies before `elide install` is run. - allPrepTasks.addAll(installMavenDepsSupport(project, extension, mustGenerateManifest)); - installerEnabled = true; - } - if (installerEnabled && hasProjectManifest) { - // if a project manifest is present, we should run `elide install` regardless of other criteria, as it - // may install non-JVM dependencies on the user's behalf. - shouldRunInstallWithOrWithoutMaven = true; - } - if (shouldRunInstallWithOrWithoutMaven) { - // add a precursor task to run `elide install`. - allPrepTasks.add(project.getTasks().create(ElideTaskName.ELIDE_TASK_INSTALL, Task.class, task -> { - task.setGroup("Elide"); - task.setDescription("Runs `elide install` to prepare the project for compilation."); - task.dependsOn(allPrepTasks.stream().filter(it -> it != task).collect(Collectors.toList())); - task.doLast(_ -> { - var start = System.currentTimeMillis(); - project.getLogger().info("Running `elide install`"); - var result = callElideCaptured(elideResolved, new String[]{"install"}); - var end = System.currentTimeMillis(); - project.getLogger().info(result); - project.getLogger().lifecycle("`elide install` completed in {}ms", (end - start)); - }); - })); - } - if (!allPrepTasks.isEmpty() && javacTasks != null && !javacTasks.isEmpty()) { - javacTasks.forEach(javacTask -> { - javacTask.dependsOn(allPrepTasks); - }); - } - }); - } -} diff --git a/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideTaskName.java b/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideTaskName.java deleted file mode 100644 index e518fb6..0000000 --- a/elide-gradle-plugin/src/main/java/dev/elide/gradle/ElideTaskName.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.elide.gradle; - -public abstract class ElideTaskName { - public static final String ELIDE_TASK_INSTALL = "elideInstall"; - public static final String ELIDE_BUILD = "elideBuild"; - public static final String ELIDE_TEST = "elideTest"; -} diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideExtension.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideExtension.kt new file mode 100644 index 0000000..fe5f829 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideExtension.kt @@ -0,0 +1,112 @@ +package dev.elide.gradle + +import dev.elide.gradle.annotation.ElideGradleDsl +import dev.elide.gradle.cli.ElideCli +import dev.elide.gradle.configuration.ElideSettings +import dev.elide.gradle.service.ElideThreadPoolService +import dev.elide.gradle.task.exec.ElideCliExec +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.register + +/** + * Main extension for the Elide Gradle plugin. + * + * This class is registered under the `elide` block in the Gradle DSL and serves as the entry point for all + * Elide-related configuration. It allows customization of build settings, runtime dependencies, and other + * Elide-specific features. + * + * Example usage in a build script: + * ```kotlin + * elide { + * settings { + * // configure Elide settings here + * } + * } + * ``` + * + * @param project The Gradle project instance where this extension is applied. + */ +@ElideGradleDsl +public open class ElideExtension internal constructor( + private val project: Project, + private val elideCli: Provider, + private val elideThreadPoolService: Provider, + private val cliPreparatoryTasks: Array>, +) { + /** + * Configuration block for general Elide plugin settings. + * + * Provides access to project-level options such as build modes, target platforms, binary version, + * and other behavior flags. + */ + public val settings: ElideSettings = ElideSettings(project) + + /** + * Applies an action to configure [ElideSettings] using the Gradle DSL. + * + * This allows fluent DSL usage such as: + * ```kotlin + * elide.settings { + * features.enableElideInstall = true + * } + * ``` + * + * @param action A Gradle [Action] that operates on the [settings] instance. + */ + public fun settings(action: Action) { + action.execute(settings) + } + + /** + * Provides access to the core set of Elide runtime libraries, versioned according to the + * configured Elide binary version. + * + * This includes Maven coordinates for Elide modules such as: + * - `elide-core`: Pure Kotlin, cross-platform foundational utilities (annotations, encoding, crypto) + * - `elide-base`: MPP data structures, logging, and general-purpose helpers + * - `elide-graalvm`: Integration layer between Elide and GraalVM for native builds + * + * These libraries form the base runtime for most Elide projects, enabling Elide's core features and + * compatibility across Kotlin/JVM, native image, and multiplatform contexts. + * + * Example usage: + * ```kotlin + * dependencies { + * implementation(elide.runtime.core) + * implementation(elide.runtime.graalvm) + * } + * ``` + * + * See the [ElideLibraries] class for details on each module. + */ + public val runtime: ElideLibraries = ElideLibraries(settings.binary.version) + + /** + * Creates a task named [taskName] of type [ElideCliExec] with necessary defaults from a Gradle Plugin + * and its extension. + * + * @param taskName The name of the task that will be created. + */ + public fun exec( + taskName: String, + configure: Action, + ): TaskProvider { + return project.tasks.register(taskName) { + dependsOn(cliPreparatoryTasks) + + cli.set(elideCli) + debug.set(settings.diagnostics.debug) + verbose.set(settings.diagnostics.verbose) + telemetry.set(settings.diagnostics.telemetry) + binPath.set(binPath) + + usesService(elideThreadPoolService) + + configure.execute(this) + } + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideGradlePlugin.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideGradlePlugin.kt new file mode 100644 index 0000000..37aac47 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideGradlePlugin.kt @@ -0,0 +1,355 @@ +package dev.elide.gradle + +import dev.elide.gradle.cli.ElideCli +import dev.elide.gradle.configuration.ElideDiagnosticsConfiguration +import dev.elide.gradle.configuration.ElideSettings +import dev.elide.gradle.configuration.binary.ElideBinaryResolutionSource +import dev.elide.gradle.configuration.features.ElideFeaturesConfiguration +import dev.elide.gradle.configuration.features.ElideIntegrationStrategy +import dev.elide.gradle.internal.Platform +import dev.elide.gradle.internal.elideDebug +import dev.elide.gradle.internal.mapNotNull +import dev.elide.gradle.service.ElideThreadPoolService +import dev.elide.gradle.task.ElideCheckVersionTask +import dev.elide.gradle.task.GenerateModuleBuildConfigurationTask +import dev.elide.gradle.task.PrepareElideJavadocShimTask +import dev.elide.gradle.task.download.DownloadElideBinaryTask +import dev.elide.gradle.task.download.ExtractElideBinaryTask +import dev.elide.gradle.task.download.PrepareDownloadedElideBinary +import dev.elide.gradle.task.download.VerifyElideBinaryTask +import dev.elide.gradle.task.exec.ElideCliExec +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.ModuleDependency +import org.gradle.api.artifacts.repositories.MavenArtifactRepository +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.javadoc.Javadoc +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.isSameFileAs +import kotlin.io.path.toPath + +public class ElideGradlePlugin : Plugin { + internal companion object { + const val THREAD_POOL_SERVICE_NAME = "elideThreadPoolService" + } + + override fun apply(target: Project) { + val extension = target.extensions.create( + name = "elide", + constructionArguments = arrayOf(target) + ) + val settings = extension.settings + + target.gradle.sharedServices.registerIfAbsent( + THREAD_POOL_SERVICE_NAME, ElideThreadPoolService::class.java, + ) + + val localCliPathProvider = target.objects.directoryProperty().fileProvider( + settings.binary.resolutionSource.flatMap { + ElideCli.resolvePathToCli(target.logger, target.providers) + .orElse( + settings.binary.resolutionSource.mapNotNull { + when (it) { + is ElideBinaryResolutionSource.LocalOnly -> null + is ElideBinaryResolutionSource.LocalIfApplicable -> it.downloadPath + is ElideBinaryResolutionSource.Project -> it.downloadPath + } + } + ) + }.map { it.toFile() } + ) + + val cliDownloadPath = target.objects.directoryProperty().fileProvider( + target.provider { + if (settings.binary.resolutionSource.get() is ElideBinaryResolutionSource.LocalOnly) + return@provider null + + if (settings.binary.version.orNull == null) + error("Elide binary resolution source is set not to be local-only, but version isn't provided.") + + target.layout + .buildDirectory + .dir("elide-runtime/bin") + .map { it.dir("${settings.binary.version.get()}-${Platform.platformClassifier}") } + .get() + .asFile + } + ) + + val resultingCliPath = localCliPathProvider.orElse(cliDownloadPath).map { + it.file(ElideCli.ELIDE_BINARY_NAME).asFile.toPath() + } + + val elideCli = ElideCli( + cwd = target.objects.directoryProperty().fileProvider( + target.layout.buildDirectory.dir("elide-runtime").map { it.asFile } + ), + bin = resultingCliPath, + providerFactory = target.providers, + ) + + val downloadElideCli = target.tasks.register("downloadElideCli") { + targetDirectory.set(cliDownloadPath) + resolutionSource.set(settings.binary.resolutionSource) + targetVersion.set(settings.binary.version) + } + + val verifyBinaryCli = target.tasks.register("verifyElideCli") { + dependsOn(downloadElideCli) + + targetDirectory.set(cliDownloadPath) + } + + val extractElideCli = target.tasks.register("extractElideCli") { + dependsOn(verifyBinaryCli) + + targetDirectory.set(cliDownloadPath) + } + + target.tasks.register("prepareDownloadedElideCli") { + dependsOn(extractElideCli) + + downloadedElideBinary.set( + target.objects + .fileProperty() + .fileProvider(cliDownloadPath.file(ElideCli.ELIDE_BINARY_NAME).map { it.asFile }) + ) + } + + target.tasks.register("checkElideCliVersion") { + cli.set(elideCli) + binPath.set(resultingCliPath.map { it.toRealPath().absolutePathString() }) + resolutionSource.set(extension.settings.binary.resolutionSource) + strictVersionCheck.set(extension.settings.binary.strictVersionCheck) + targetVersion.set(extension.settings.binary.version) + silentMode.set(extension.settings.binary.silentMode) + } + + target.tasks.register("generateModuleMetadataForElide") { + onlyIf { extension.settings.features.generatePklBuildConfiguration.get() } + + // Main dependencies + // Elide for now can't distinguish implementation, compileOnly or runtimeOnly, so we put it all in one + mainDeclaredDependencies.set( + target.provider { + listOf("implementation", "compileOnly", "runtimeOnly") + .mapNotNull { configName -> + target.configurations.findByName(configName) + } + .flatMap { config -> + config.dependencies.withType().mapNotNull { dep -> + val group = dep.group ?: return@mapNotNull null + val name = dep.name + val version = dep.version ?: return@mapNotNull null + "$group:$name:$version" + } + } + } + ) + + // Test dependencies + // Elide for now can't distinguish implementation, compileOnly or runtimeOnly, so we put it all in one + testDeclaredDependencies.set( + target.provider { + listOf("testImplementation", "testCompileOnly", "testRuntimeOnly") + .mapNotNull { configName -> + target.configurations.findByName(configName) + } + .flatMap { config -> + config.dependencies.withType().mapNotNull { dep -> + val group = dep.group ?: return@mapNotNull null + val name = dep.name + val version = dep.version ?: return@mapNotNull null + "$group:$name:$version" + } + } + } + ) + + declaredRepositories.set( + target.provider { + target.repositories.filterIsInstance() + .filter { repo -> + repo.url.scheme != "file" || + repo.url.toPath().isSameFileAs( + extension.settings.devRoot.dir("m2/dependencies").get().asFile.toPath() + ) + } + } + ) + + generatedFile.set( + target.layout + .buildDirectory + .dir("elide-runtime/generated") + .map { it.file("module.pkl") } + ) + } + + val elideInstall = extension.exec("elideInstall") { + group = "Elide" + description = "Runs `elide install` to prepare the project for compilation." + + args.add("install") + args.add(telemetry.mapNotNull { enabled -> if (!enabled) "--no-telemetry" else null }) + + onlyIf("`elide.settings.manifest` is not set or target file does not exists") { + settings.manifest.isPresent && settings.manifest.asFile.get().exists() + } + + onlyIf("`elide.settings.features.enableElideInstall` is false") { + settings.features.enableElideInstall.get() + } + } + + val prepareElideJavadocShimTask = target.tasks.register( + name = "prepareElideJavadocShim" + ) { + cliPath.set(resultingCliPath.get().absolutePathString()) + shimFile.set(target.layout.buildDirectory.file("elide-runtime/shim/javadoc")) + } + + target.applyMavenLocalRepo(settings) + target.overrideJavaCompileIfEnabled( + featuresConfig = settings.features, + diagnosticsConfig = settings.diagnostics, + cliPath = resultingCliPath, + elideInstall = elideInstall, + ) + target.overrideJavadocIfEnabled( + featuresConfig = settings.features, + cliPath = resultingCliPath, + prepareElideJavadocShimTask = prepareElideJavadocShimTask, + silentMode = settings.binary.silentMode, + shimPath = prepareElideJavadocShimTask.flatMap { file -> + file.shimFile.asFile.map { it.absolutePath } + }, + ) + + target.configurations.configureEach { + incoming.beforeResolve { + // Let's validate that 'elideInstall' is there + if (settings.features.enableElideInstall.get() && !target.gradle.taskGraph.hasTask(elideInstall.get())) { + error("'elideInstall' should run before any dependency resolution. Did somebody started resolution too early (e.g on configuration phase)?") + } + } + } + } + + /** + * Adds the maven local repository pointing to the "[dev.elide.gradle.configuration.ElideSettings.devRoot]/dependencies/m2" folder. + * + * This function also reorders the list of maven repositories to ensure that local repository is the first one to be used. + */ + private fun Project.applyMavenLocalRepo(extension: ElideSettings) { + val localM2DirPath = extension.devRoot + .get() + .dir("dependencies") + .dir("m2") + .asFile + .also { it.mkdirs() } + .absolutePath + + val indexOfLocalElideRepo = repositories.indexOfFirst { + it is MavenArtifactRepository + && it.url.scheme == "file" + && it.url.path == localM2DirPath + }.takeIf { it != -1 } + + // early return in case if it's already the first one. + if (indexOfLocalElideRepo == 0) return + + if (indexOfLocalElideRepo != null) { + repositories.removeAt(indexOfLocalElideRepo) + } + + // We need to ensure that our maven local repository is the first one, for not to hit + // remote ones. + val snapshot = repositories.toList() + repositories.clear() + repositories.mavenLocal { + uri("file://$localM2DirPath") + } + snapshot.forEach(repositories::add) + } + + private fun Project.overrideJavaCompileIfEnabled( + featuresConfig: ElideFeaturesConfiguration, + diagnosticsConfig: ElideDiagnosticsConfiguration, + cliPath: Provider, + elideInstall: TaskProvider, + ) { + afterEvaluate { + val javacStrategy = featuresConfig.javacStrategy.get() + + if (javacStrategy == ElideIntegrationStrategy.NO_ELIDE) { + logger.elideDebug("Skipping altering JavaCompile tasks: ElideIntegrationStrategy is NO_ELIDE.") + return@afterEvaluate + } + + if (cliPath.orNull?.exists() != true && gradle.startParameter.isOffline) { + logger.elideDebug("Skipping altering JavaCompile tasks: elide is not found and Gradle is in offline mode.") + return@afterEvaluate + } + + tasks.withType().configureEach { + dependsOn(elideInstall) + + options.isFork = true + options.forkOptions { + executable = cliPath.get().absolutePathString() + jvmArgs = buildList { + if (diagnosticsConfig.debug.get()) + add("--debug") + if (diagnosticsConfig.verbose.get()) + add("--verbose") + + add("javac") + add("--") + jvmArgs.orEmpty().forEach { + add(it) + } + } + } + } + } + } + + private fun Project.overrideJavadocIfEnabled( + featuresConfig: ElideFeaturesConfiguration, + cliPath: Provider, + shimPath: Provider, + prepareElideJavadocShimTask: TaskProvider, + silentMode: Provider, + ) { + afterEvaluate { + val javadocStrategy = featuresConfig.javadocStrategy.get() + + if (javadocStrategy == ElideIntegrationStrategy.NO_ELIDE) { + logger.elideDebug("Skipping altering Javadoc tasks: ElideIntegrationStrategy is NO_ELIDE.") + return@afterEvaluate + } + + if (cliPath.orNull == null && !silentMode.get()) { + error("CLI path has not been found. Have you set local-only?") + } + + if (cliPath.orNull?.exists() != true && gradle.startParameter.isOffline && silentMode.get()) { + logger.elideDebug("Skipping altering Javadoc tasks: elide is not found and Gradle is in offline mode.") + return@afterEvaluate + } + + tasks.withType().configureEach { + executable = shimPath.get() + dependsOn(prepareElideJavadocShimTask) + } + } + } +} diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideLibraries.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideLibraries.kt new file mode 100644 index 0000000..7ab31d3 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/ElideLibraries.kt @@ -0,0 +1,53 @@ +package dev.elide.gradle + +import org.gradle.api.provider.Provider + +public class ElideLibraries( + elideVersion: Provider +) { + /** + * Provides the Maven coordinate for the `elide-core` artifact, using the + * configured in [dev.elide.gradle.configuration.ElideSettings.binary] Elide version. + * + * The `elide-core` module offers universal, pure-Kotlin utilities and declarations + * that form the foundational layer of the Elide framework and runtime. It ensures + * broad platform compatibility by relying only on Kotlin team's official dependencies, + * such as the Kotlin standard library and KotlinX. + * + * This module includes cross-platform APIs, annotations, encoding utilities (Base64, Hex), + * cryptography enumerations, and platform-specific defaults. It is designed for + * maximum portability and is widely used throughout Elide tooling and applications. + * + * **[Learn more](https://github.com/elide-dev/elide/blob/main/packages/core/module.md)** + */ + public val core: Provider = elideVersion.map { "dev.elide:elide-core:$it" } + + /** + * Provides the Maven coordinate for the `elide-core` artifact, using the + * configured in [dev.elide.gradle.configuration.ElideSettings.binary] Elide version. + * + * - **Annotations:** Common annotations used across the Elide framework and runtime + * - **Codecs:** Multiplatform-capable implementations of Base64, hex, and other common encoding tools + * - **Crypto:** Core crypto, hashing, and entropy primitives (for example, `UUID`) + * - **Structures:** Data structures in MPP and pure Kotlin for sorted maps, sets, and lists + * - **Logging:** Multiplatform-capable logging, which delegates to platform logging tools + * + * **[Learn more](https://github.com/elide-dev/elide/blob/main/packages/base/module.md)** + */ + public val base: Provider = elideVersion.map { "dev.elide:elide-base:$it" } + + /** + * Provides the Maven coordinate for the `elide-graalvm` artifact, using the + * configured in [dev.elide.gradle.configuration.ElideSettings.binary] Elide version. + * + * The `elide-graalvm` module serves as the main integration layer between Elide and GraalVM, + * and powers the core of the Elide runtime. It enables Elide to operate in environments + * where GraalVM is used, supporting GraalVM-specific features and runtime behavior. + * + * This module is central to Elide’s native execution model and is typically included in projects + * targeting GraalVM or native image builds. + * + * **[Learn more](https://github.com/elide-dev/elide/blob/main/packages/graal/module.md)** + */ + public val graalvm: Provider = elideVersion.map { "dev.elide:elide-graalvm:$it" } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/annotation/ElideGradleDsl.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/annotation/ElideGradleDsl.kt new file mode 100644 index 0000000..a38b90a --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/annotation/ElideGradleDsl.kt @@ -0,0 +1,4 @@ +package dev.elide.gradle.annotation + +@DslMarker +public annotation class ElideGradleDsl \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCli.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCli.kt new file mode 100644 index 0000000..86e8ab7 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCli.kt @@ -0,0 +1,197 @@ +package dev.elide.gradle.cli + +import dev.elide.gradle.internal.elideInfo +import dev.elide.gradle.internal.mapNotNull +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.logging.Logger +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.nio.file.Path +import java.util.concurrent.Executor +import java.util.concurrent.ThreadPoolExecutor +import kotlin.io.path.* + +public class ElideCli( + private val cwd: DirectoryProperty, + private val bin: Provider, + private val providerFactory: ProviderFactory, +) { + /** + * Runs `elide --version` in terminal getting the result. + * + * @param bin Path to a binary file. + * @return [String] of the output, excluding error output. + */ + public fun getVersion(executor: Executor): ElideCliInvocationResult { + return createInvocation(args = listOf("--version"), executor) + .withCapturedStdout() + .mapSuccess { input -> + input.trim() + }.execute() + } + + public fun createInvocation(args: List, executor: Executor): ElideCliInvocation { + try { + val stdout = ProxyInputStream() + val stderr = ProxyInputStream() + + val action: Provider> = providerFactory.provider { + val pathToElide = bin.get().pathString + val allArgs = listOf(pathToElide) + args + val cwdFile = cwd.get().asFile + + val builder = ProcessBuilder(allArgs).directory(cwdFile) + val proc = builder.start() + + stdout.target = proc.inputStream + stderr.target = proc.errorStream + val exitCode = proc.waitFor() + + val result = try { + if (exitCode == 0) { + ElideCliInvocationResult.Success(Unit) + } else { + ElideCliInvocationResult.ExitFailure(exitCode) + } + } catch (e: Exception) { + ElideCliInvocationResult.Error(e) + } + + ElideCliInvocationResultWithThreadPool(executor, result) + } + + // Return invocation with PipedInputStreams (to be read by consumer) + return ElideCliInvocation(action, stdout, stderr) + } catch (e: InterruptedException) { + throw IllegalStateException("Failed to wait for Elide process", e) + } catch (e: IOException) { + throw IllegalStateException("Failed to start Elide captured process", e) + } + } + + internal companion object { + const val ELIDE_BINARY_NAME: String = "elide" + + /** + * Resolves a path to elide from $PATH or home directory. + * Unless present, returns nothing. + */ + internal fun resolvePathToCli( + logger: Logger, + providers: ProviderFactory, + ): Provider { + fun logNonExecutable(path: Path) = logger.elideInfo( + "Elide binary found at '${path.absolutePathString()}' in \$PATH, but it is not executable. " + + "Maybe try `chmod +x ${path.absolutePathString()}`? Skipping." + ) + + val elideFromProperties: Provider = providers.gradleProperty("dev.elide.gradle.binPath") + .map { + Path(it).also { path -> + require(path.exists()) { + "Elide binary specified explicitly via `dev.elide.gradle.binPath` does not exist." + } + require(!path.isDirectory()) { + "Elide binary specified via `dev.elide.gradle.binPath` is a directory, but binary executable is expected." + } + require(path.isExecutable()) { + "Elide binary specified via `dev.elide.gradle.binPath` is not an executable. Maybe try `chmod +x ${path.absolutePathString()}`?" + } + } + } + + val elideFromPath = providers.environmentVariable("PATH") + .mapNotNull { value -> + value.split(File.pathSeparator) + .asSequence() + .map { Path(it).resolve(ELIDE_BINARY_NAME) } + .onEach { + // Let's notify user that elide that is resolved + // is not executable. + if (!it.isExecutable()) + logNonExecutable(it) + } + .filter { it.exists() && it.isExecutable() } + .firstOrNull() + } + + val elideFromUserHome = providers.systemProperty("user.home") + .mapNotNull { pathText -> + Path(pathText).resolve("elide") + .resolve(ELIDE_BINARY_NAME) + .takeIf { it.exists() && it.isRegularFile() } + ?.let { file -> + if (!file.isExecutable()) { + logNonExecutable(file) + } else { + return@mapNotNull file + } + } + null + } + + return elideFromProperties + .orElse(elideFromPath) + .orElse(elideFromUserHome) + } + } + + private class ProxyInputStream( + var target: InputStream? = null + ) : InputStream() { + + override fun read(): Int { + val current = target + return if (current != null) { + current.read() + } else { + // Block or idle until assigned — here we just simulate no-read + // Returning 0 would mean "read 0 bytes" (but InputStream.read() returns a single byte or -1) + // So we wait until assigned + while (target == null) { + Thread.sleep(10) + } + target!!.read() + } + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + val current = target + return if (current != null) { + current.read(b, off, len) + } else { + while (target == null) { + Thread.sleep(10) + } + target!!.read(b, off, len) + } + } + + override fun available(): Int { + return target?.available() ?: 0 + } + + override fun close() { + target?.close() + } + + override fun skip(n: Long): Long { + return target?.skip(n) ?: 0 + } + + override fun mark(readlimit: Int) { + target?.mark(readlimit) + } + + override fun reset() { + target?.reset() + } + + override fun markSupported(): Boolean { + return target?.markSupported() ?: false + } + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocation.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocation.kt new file mode 100644 index 0000000..7deb376 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocation.kt @@ -0,0 +1,264 @@ +package dev.elide.gradle.cli + +import dev.elide.gradle.cli.ElideCliInvocationResult.* +import org.gradle.api.Transformer +import org.gradle.api.provider.Provider +import java.io.InputStream +import java.io.OutputStream +import java.util.function.Consumer +import java.util.stream.Stream +import kotlin.streams.asStream + +/** + * Represents a lazily evaluated invocation of the Elide CLI, encapsulating both the execution result and + * live access to the process's standard output and error streams. + * + * @param T The type of the successful invocation result value. + * @property operation A [Provider] that, when queried, executes the CLI invocation and returns its result. + * @property stdOut The [OutputStream] receiving the live standard output of the CLI process. + * @property stdErr The [OutputStream] receiving the live standard error output of the CLI process. + * + * This class supports functional-style transformations of the invocation result while preserving + * streaming output access. It does not buffer output by default, enabling live logging or processing. + * + * Usage pattern: + * ``` + * val invocation: ElideCliInvocation = ... + * val result = invocation.execute() + * ``` + * + * Transformations like `mapSuccess` and `map` create new invocations with transformed results, + * allowing composable processing pipelines. + * + * The provided methods `withCapturedStdout()` and `withCapturedStdoutAndStderr()` enable optional + * capturing of output streams into strings, while still streaming data live to the original streams. + */ +public class ElideCliInvocation internal constructor( + private val operation: Provider>, + public val stdOut: InputStream, + public val stdErr: InputStream, +) { + /** + * Executes the CLI invocation by querying the underlying [operation] provider. + * This triggers the actual process execution and returns the result. + * + * @return The result of the invocation, either [ElideCliInvocationResult.Success] or [ElideCliInvocationResult.ExitFailure]. + */ + public fun execute(): ElideCliInvocationResult = operation.get().result + + /** + * Transforms the successful result value using the provided [transformer]. + * If the invocation failed, the failure is propagated unchanged. + * + * @param R The type of the transformed success value. + * @param transformer The transformation function from [T] to [R]. + * @return A new [ElideCliInvocation] with the transformed result type. + */ + public fun mapSuccess( + transformer: Transformer, + ): ElideCliInvocation { + return map { (_, result) -> + when (result) { + is ExitFailure, is Error -> result as ElideCliInvocationResult + is Success -> Success(transformer.transform(result.value)) as ElideCliInvocationResult + } + } + } + + /** + * Consumes the standard output (stdout) of the invocation by passing a [Stream] of lines + * to the given [consumer]. The lines are read lazily and provided within the executor's + * context to ensure asynchronous execution. + * + * This method does not block the calling thread; instead, it schedules the consumption + * of stdout on the associated executor. The original invocation result is returned unchanged. + * + * **Warning**: [stdOut] as well as [stdErr] is one-shot by default. + * + * @param consumer A [Consumer] that accepts a [Stream] of stdout lines. + * @return A new [ElideCliInvocation] with the same result type [T], where stdout consumption + * is performed asynchronously as a side effect. + */ + public fun consumeStdout(consumer: Consumer>): ElideCliInvocation { + return map { (executor, result) -> + executor.execute { + stdOut.bufferedReader().useLines { lines -> + consumer.accept(lines.asStream()) + } + } + + result + } + } + + /** + * Consumes the standard error (stderr) output of the invocation by passing a [Stream] of lines + * to the given [consumer]. The lines are read lazily and provided within the executor's + * context to ensure asynchronous execution. + * + * This method does not block the calling thread; instead, it schedules the consumption + * of stderr on the associated executor. The original invocation result is returned unchanged. + * + * @param consumer A [Consumer] that accepts a [Stream] of stderr lines. + * @return A new [ElideCliInvocation] with the same result type [T], where stderr consumption + * is performed asynchronously as a side effect. + */ + public fun consumeStderr(consumer: Consumer>): ElideCliInvocation { + return map { (executor, result) -> + executor.execute { + stdErr.bufferedReader().useLines { lines -> + consumer.accept(lines.asStream()) + } + } + + result + } + } + + /** + * Invokes the provided [consumer] if the invocation was successful, + * passing it the success value. + * + * This allows side effect handling on success without modifying the result. + * + * @param consumer a [Consumer] that accepts the success value of type [T]. + * @return a new [ElideCliInvocation] with the same result type [T]. + */ + public fun onSuccess(consumer: Consumer): ElideCliInvocation { + return map { (_, result) -> + if (result is Success) { + consumer.accept(result.value) + } + result + } + } + + /** + * Invokes the provided [consumer] if the invocation failed with a non-zero exit code, + * passing it the exit code. + * + * This allows side-effect handling on process failures without modifying the result. + * + * @param consumer a [Consumer] that accepts the non-success exit code. + * @return a new [ElideCliInvocation] with the same result type [T]. + */ + public fun onNonZeroExitCode(consumer: Consumer): ElideCliInvocation { + return map { (_, result) -> + if (result is ExitFailure) { + consumer.accept(result.exitCode) + } + result + } + } + + /** + * Invokes the provided [consumer] if the invocation resulted in an exception, + * passing it the thrown [Exception]. + * + * This allows side-effect handling on execution errors without modifying the result. + * + * @param consumer a [Consumer] that accepts the [Exception] thrown during invocation. + * @return a new [ElideCliInvocation] with the same result type [T]. + */ + public fun onException(consumer: Consumer): ElideCliInvocation { + return map { (_, result) -> + if (result is Error) { + consumer.accept(result.exception) + } + result + } + } + + /** + * Transforms the entire invocation result using the provided [transformer]. + * + * @param R The type of the transformed invocation result's success value. + * @param transformer The transformation function from [ElideCliInvocationResult] to [ElideCliInvocationResult]. + * @return A new [ElideCliInvocation] with the transformed result type. + */ + public fun map( + transformer: Transformer, ElideCliInvocationResultWithThreadPool> + ): ElideCliInvocation { + return ElideCliInvocation( + operation = operation.map { input -> + val result = transformer.transform(input) + ElideCliInvocationResultWithThreadPool(input.executor, result) + }, + stdOut = stdOut, + stdErr = stdErr, + ) + } + + /** + * Returns a new [ElideCliInvocation] that captures the standard output into a [String] + * while still streaming it live to the original [stdOut]. + * + * The captured output is available as the success value of the transformed invocation result. + * + * @return A new [ElideCliInvocation] with the success value being the captured standard output as a trimmed string. + */ + public fun withCapturedStdout(): ElideCliInvocation { + return map { (pool, result) -> + val captureBuffer = StringBuilder() + + pool.execute { + stdOut.bufferedReader().useLines { lines -> + lines.forEach { + captureBuffer.append(it) + } + } + } + + when (result) { + is ExitFailure, is Error -> result + is Success<*> -> { + val output = captureBuffer.toString() + Success(output.trim()) + } + } as ElideCliInvocationResult + } + } + + /** + * Returns a new [ElideCliInvocation] that captures the standard and error output into a [String]. + * + * The captured output is available as the success value of the transformed invocation result. + * + * @return A new [ElideCliInvocation] with the success value being the captured standard output as a trimmed string. + */ + public fun withCapturedStdoutAndStderr(): ElideCliInvocation { + return map { (pool, result) -> + val captureBuffer = StringBuffer() + + pool.execute { + stdOut.bufferedReader().useLines { lines -> + lines.forEach { + captureBuffer.append(it) + } + } + } + + pool.execute { + stdErr.bufferedReader().useLines { lines -> + lines.forEach { + captureBuffer.append(it) + } + } + } + + when (result) { + is ExitFailure, is Error -> result + is Success<*> -> { + val output = captureBuffer.toString() + Success(output.trim()) + } + } as ElideCliInvocationResult + } + } + + @JvmOverloads + public fun buffered(stdOutBufferSize: Int = DEFAULT_BUFFER_SIZE, stdErrBufferSize: Int = DEFAULT_BUFFER_SIZE): ElideCliInvocation { + return ElideCliInvocation(operation, stdOut.buffered(stdOutBufferSize), stdErr.buffered(stdOutBufferSize)) + } +} + diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocationResult.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocationResult.kt new file mode 100644 index 0000000..921e41f --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocationResult.kt @@ -0,0 +1,23 @@ +package dev.elide.gradle.cli + +public sealed interface ElideCliInvocationResult { + public data class Success( + val value: T, + ) : ElideCliInvocationResult + + /** Represents a process that completed but exited with a non-zero exit code. */ + public data class ExitFailure( + val exitCode: Int, + ) : ElideCliInvocationResult, ElideCliInvocationFailure + + /** Represents an unexpected error during execution (e.g. IO failure). */ + public data class Error( + val exception: Exception, + ) : ElideCliInvocationResult, ElideCliInvocationFailure +} + +public sealed interface ElideCliInvocationFailure + +public fun ElideCliInvocationResult.getSuccessValueOrElse(block: (ElideCliInvocationFailure) -> T): T { + return (this as? ElideCliInvocationResult.Success)?.value ?: block(this as ElideCliInvocationResult.ExitFailure) +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocationResultWithThreadPool.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocationResultWithThreadPool.kt new file mode 100644 index 0000000..02315ca --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/cli/ElideCliInvocationResultWithThreadPool.kt @@ -0,0 +1,8 @@ +package dev.elide.gradle.cli + +import java.util.concurrent.Executor + +public data class ElideCliInvocationResultWithThreadPool( + public val executor: Executor, + public val result: ElideCliInvocationResult, +) \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/ElideDiagnosticsConfiguration.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/ElideDiagnosticsConfiguration.kt new file mode 100644 index 0000000..e8a25e8 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/ElideDiagnosticsConfiguration.kt @@ -0,0 +1,53 @@ +package dev.elide.gradle.configuration + +import dev.elide.gradle.annotation.ElideGradleDsl +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.property + +@ElideGradleDsl +public class ElideDiagnosticsConfiguration(private val project: Project) { + private inline val objects get() = project.objects + private inline val providers get() = project.providers + + /** + * Enables debug logging for Elide and prints debug information to the terminal. + * + * This property also respects the `--debug` flag passed to Gradle. In addition, + * you may use `dev.elide.gradle.diagnostics.debug`. + */ + public val debug: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.diagnostics.debug") + .map(String::toBooleanStrict) + .orElse(project.provider { project.logger.isDebugEnabled }) + ) + + /** + * Enables verbose logging for the plugin. + * + * This property also respects the `--info` flag passed to Gradle, + * which activates Gradle's own «verbose» mode. In addition, + * you may use `dev.elide.diagnostics.verbose` property. + */ + public val verbose: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.diagnostics.verbose") + .map(String::toBooleanStrict) + .orElse(project.provider { project.logger.isInfoEnabled }) + ) + + /** + * Sets enable/disable status on all telemetry features within Elide. Applies + * only to the tasks owned by the Gradle Plugin, otherwise you should manage it yourself + * considering this property or an alias provided in [dev.elide.gradle.task.exec.ElideCliExec]. + * + * This behavior can also be changed via `dev.elide.gradle.diagnostics.telemetry` property. + */ + public val telemetry: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.diagnostics.telemetry") + .map(String::toBooleanStrict) + .orElse(true) + ) +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/ElideSettings.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/ElideSettings.kt new file mode 100644 index 0000000..2b7839c --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/ElideSettings.kt @@ -0,0 +1,87 @@ +package dev.elide.gradle.configuration + +import dev.elide.gradle.annotation.ElideGradleDsl +import dev.elide.gradle.configuration.binary.ElideBinaryConfiguration +import dev.elide.gradle.configuration.features.ElideFeaturesConfiguration +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.Internal + +@ElideGradleDsl +public class ElideSettings( + project: Project, +) { + @Internal + private val objects = project.objects + + /** + * Sets the path to the project manifest, expressed in Pkl format. Elide project manifests can + * specify dependencies, build scripts, and other project metadata. Defaults to `elide.pkl` and + * automatically finds any present `elide.pkl` in the active project. + */ + public val manifest: RegularFileProperty = objects.fileProperty() + .convention(project.layout.projectDirectory.file("elide.pkl")) + + /** + * Sets the path to the internal hidden folder of the Elide. The default is + * the `.dev` inside target project (module). + */ + public val devRoot: DirectoryProperty = objects.directoryProperty() + .convention(project.layout.projectDirectory.dir(".dev")) + + /** + * Configuration block for enabling or disabling key Elide integration features in the Gradle build. + * + * This includes options for dependency resolution, Maven integration, project awareness, and Java compiler strategy. + * The settings here control how Elide interacts with Gradle and the build lifecycle, enabling opt-in or experimental + * behaviors while maintaining sensible defaults. + * + * For example, enabling Elide's dependency resolver activates downloading and managing dependencies through Elide, + * while the Java compiler strategy controls whether to use Gradle’s default `javac`, prefer Elide's optimized + * compiler, or enforce strict use of Elide’s compiler. + * + * Typical usage: + * ``` + * elide { + * features { + * enableElideInstall = true + * enableMavenIntegration = false + * + * javacStrategy = JavacIntegrationStrategy.PREFER_ELIDE + * } + * } + * ``` + */ + public val features: ElideFeaturesConfiguration = ElideFeaturesConfiguration(project, manifest) + + /** + * Configuration for controlling diagnostic options, such as debug and verbose logging. + * + * Use this block to adjust the level of logging and diagnostic output + * emitted by the Elide. + */ + public val diagnostics: ElideDiagnosticsConfiguration = ElideDiagnosticsConfiguration(project) + + /** + * Configuration for managing the Elide binary executable used by the plugin. + * + * Includes settings for binary path overrides, version pinning, and resolution strategy + * (e.g., whether to use a local binary or download a project-scoped one). + */ + public val binary: ElideBinaryConfiguration = ElideBinaryConfiguration(project) + + + public fun diagnostics(action: Action) { + action.execute(diagnostics) + } + + public fun features(action: Action) { + action.execute(features) + } + + public fun binary(action: Action) { + action.execute(binary) + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/binary/ElideBinaryConfiguration.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/binary/ElideBinaryConfiguration.kt new file mode 100644 index 0000000..3a01790 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/binary/ElideBinaryConfiguration.kt @@ -0,0 +1,139 @@ +package dev.elide.gradle.configuration.binary + +import dev.elide.gradle.annotation.ElideGradleDsl +import org.gradle.api.Incubating +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.property +import java.io.File + +@ElideGradleDsl +public class ElideBinaryConfiguration(private val project: Project) { + private inline val objects get() = project.objects + private inline val providers get() = project.providers + + /** + * Declares whether the Elide Gradle plugin should suppress build failure if binary + * resolution fails. + * + * This only affects plugin-level behavior. It does **not guarantee** that Java or Kotlin + * compilation tasks will succeed. For example, if [resolutionSource] is not `LocalOnly` + * and the local binary cannot be used, the plugin will attempt to download the binary. + * If the download fails and **Gradle is not explicitly running in offline mode**, + * the build will still fail — even if `silentMode` is enabled. + * + * You can also configure this property via the Gradle property: + * ``` + * dev.elide.gradle.bin.silentMode=true + * ``` + */ + @Incubating + public val silentMode: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.bin.silentMode") + .map(String::toBooleanStrict) + .orElse(false) + ) + + /** + * Specifies the expected version of the Elide binary to use. + * + * The resolved binary’s version will be validated against this value. + * If not set (`null`), version validation is skipped. The absence of the value + * will fail if it's expected Elide to be downloaded. + * + * You can also configure it through `dev.elide.gradle.bin.version` gradle property. + */ + @Incubating + public val version: Property = objects.property() + .convention(providers.gradleProperty("dev.elide.gradle.bin.version")) + + /** + * Enables strict version validation for the resolved Elide binary. + * + * When set to `true`, the plugin will fail or download a new binary (depending on [resolutionSource]) + * the build if the local resolved binary's version does not exactly match the configured [version]. + * If `false`, version mismatches may be tolerated depending on the selected [resolutionSource]. + * + * You can also configure it through `dev.elide.gradle.bin.strictVersionCheck` gradle property. + */ + @Incubating + public val strictVersionCheck: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.bin.strictVersionCheck") + .map(String::toBooleanStrict) + .orElse(false) + ) + + /** + * Sets the resolution strategy when resolving the Elide's binary application. By default, we use + * locally downloaded elide unless the specified [version] is not met. This behavior might be changed + * by setting up [resolutionSource] to [ElideBinaryResolutionSource.LocalOnly] or [ElideBinaryResolutionSource.Project]. + */ + public val resolutionSource: Property = objects.property() + .convention(ElideBinaryResolutionSource.LocalOnly(null)) + + /** + * Configures the Elide binary resolution to **use only a local binary** available on the system. + * + * This disables any version requirement and clears any explicitly set binary path. + * The plugin will **not** attempt to download or resolve any binary from the project directory. + */ + public fun useLocalOnly(path: File? = null) { + version.unset() + this.strictVersionCheck.set(strictVersionCheck) + resolutionSource.set(ElideBinaryResolutionSource.LocalOnly(path?.toPath())) + } + + /** + * Configures the Elide binary resolution to **prefer a local binary** if it's present on + * the local machine. In addition, if [strictVersionCheck] is enabled, it's checked whether the version is the same. + * If the local binary is missing or incompatible, the plugin may fallback to other resolution strategies (e.g., downloading the required version). + * + * @param version The minimum compatible version string to require from the local binary. + * Must not be blank or `"latest"`. + * @param strictVersionCheck Determines whether is to check the version against given one. + * @throws IllegalArgumentException if [version] is blank or equals `"latest"`. + */ + @Incubating + public fun useLocalIfApplicable( + version: String, + strictVersionCheck: Boolean = true, + downloadPath: File? = null, + ) { + require(version.isNotBlank()) { + "elide.binary.useLocalBinaryIfApplicable does not accept blank version." + } + require(version != "latest") { + "elide.binary.useLocalBinaryIfApplicable does not accept 'latest' as a version." + } + + this.strictVersionCheck.set(strictVersionCheck) + this.version.set(version) + resolutionSource.set(ElideBinaryResolutionSource.LocalIfApplicable(downloadPath?.toPath())) + } + + /** + * Configures the Elide binary resolution to **always use a project-scoped binary**, which + * is downloaded and managed inside the project. This ignores any local binaries on the system. + * + * @param version The version string of the binary to download and use. + * Must not be blank or `"latest"`. + * @throws IllegalArgumentException if [version] is blank or equals `"latest"`. + */ + public fun useProjectBinary( + version: String, + downloadPath: File? = null, + ) { + require(version.isNotBlank()) { + "elide.binary.useLocalBinaryIfApplicable does not accept blank version." + } + require(version != "latest") { + "elide.binary.useLocalBinaryIfApplicable does not accept 'latest' as a version." + } + + this.strictVersionCheck.unset() + this.version.set(version) + resolutionSource.set(ElideBinaryResolutionSource.Project(downloadPath?.toPath())) + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/binary/ElideBinaryResolutionSource.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/binary/ElideBinaryResolutionSource.kt new file mode 100644 index 0000000..6c47128 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/binary/ElideBinaryResolutionSource.kt @@ -0,0 +1,35 @@ +package dev.elide.gradle.configuration.binary + +import java.nio.file.Path + +public sealed interface ElideBinaryResolutionSource { + /** + * Always use a locally installed Elide binary (e.g. from `$PATH` or `$HOME/elide`), regardless of version. + * + * @param path Path to elide's binary folder. If unspecified, it's resolved either from `$PATH` or from home directory, + * otherwise plugin will throw an exception. + */ + public class LocalOnly(public val path: Path? = null) : ElideBinaryResolutionSource + + /** + * Resolution strategy that prefers using a local Elide binary over downloading a new one. + * + * When enabled: + * - If strict version checking is enabled and the local binary version matches the required version, + * the plugin will **download a new binary**. + * - Otherwise, the presence of **any local Elide binary version** is sufficient to avoid downloading. + * + * @param downloadPath Optional path within the project where the plugin should download the Elide binary + * if it cannot use a preinstalled binary on the machine. + * Defaults to `build/elide-runtime/bin` if not specified. + */ + public class LocalIfApplicable(public val downloadPath: Path? = null) : ElideBinaryResolutionSource + + /** + * Always download and use the Elide binary scoped to the project. + * Local versions will be ignored. + * + * @param downloadPath Path within the project to which plugin should download the elide. + */ + public class Project(public val downloadPath: Path? = null) : ElideBinaryResolutionSource +} diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/features/ElideFeaturesConfiguration.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/features/ElideFeaturesConfiguration.kt new file mode 100644 index 0000000..083d2fb --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/features/ElideFeaturesConfiguration.kt @@ -0,0 +1,65 @@ +package dev.elide.gradle.configuration.features + +import dev.elide.gradle.annotation.ElideGradleDsl +import org.gradle.api.Incubating +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.property + +@ElideGradleDsl +public class ElideFeaturesConfiguration(private val project: Project, manifest: RegularFileProperty) { + private inline val objects get() = project.objects + private inline val providers get() = project.providers + + /** + * Determines whether to use Elide's dependency resolver and downloader. + * + * Defaults to `true` when an `elide.pkl` file is present in the project root. + */ + public val enableElideInstall: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.features.enableElideInstall") + .map(String::toBooleanStrict) + .orElse(manifest.map { it.asFile.exists() }) + ) + + /** + * Specifies whether is to use Elide's maven resolver. The difference between enabling it + * and [enableElideInstall] is that [generatePklBuildConfiguration] enables task to write a `.pkl` file + * with list of dependencies and repositories to be imported and used in a `project.pkl`. + * + * @see dev.elide.gradle.task.GenerateModuleBuildConfigurationTask + */ + public val generatePklBuildConfiguration: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.features.generatePklBuildConfiguration") + .map(String::toBooleanStrict) + ) + + /** + * Determines whether Elide should be used to compile Java instead of Gradle’s default Java compiler. This behavior + * can also be changed via `dev.elide.gradle.features.javacStrategy` property. + * + * Defaults to [ElideIntegrationStrategy.PREFER_ELIDE] when the plugin is applied to the project. + */ + public val javacStrategy: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.features.javacStrategy") + .map { ElideIntegrationStrategy.valueOf(it.uppercase().replace('-', '_')) } + .orElse(ElideIntegrationStrategy.PREFER_ELIDE) + ) + + /** + * Determines whether Elide should be used to generate `javadoc` instead of Gradle’s default. This behavior + * can also be changed via `dev.elide.gradle.features.javadocStrategy` property. + * + * Defaults to `true` when the plugin is applied to the project. + */ + public val javadocStrategy: Property = objects.property() + .convention( + providers.gradleProperty("dev.elide.gradle.features.javadocStrategy") + .map { ElideIntegrationStrategy.valueOf(it.uppercase().replace('-', '_')) } + .orElse(ElideIntegrationStrategy.PREFER_ELIDE) + ) +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/features/ElideIntegrationStrategy.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/features/ElideIntegrationStrategy.kt new file mode 100644 index 0000000..a666afa --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/configuration/features/ElideIntegrationStrategy.kt @@ -0,0 +1,19 @@ +package dev.elide.gradle.configuration.features + +public enum class ElideIntegrationStrategy { + /** + * Use Gradle's default. + */ + NO_ELIDE, + + /** + * Use Elide's Java compiler/Javadoc. In case of elide unavailability or an error, + * we fall back to Gradle's default if possible. + */ + PREFER_ELIDE, + + /** + * Use only Elide’s Java compiler/Javadoc and fail if it is not available. + */ + ELIDE_STRICT +} diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/Logger.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/Logger.kt new file mode 100644 index 0000000..2d5aab4 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/Logger.kt @@ -0,0 +1,30 @@ +package dev.elide.gradle.internal + +import org.gradle.api.logging.Logger + +internal inline fun Logger.elideDebug(message: () -> String) { + if (isDebugEnabled) { + elideDebug(message()) + } +} + +internal fun Logger.elideInfo(message: String) { + info("[ELIDE] $message") +} + +internal fun Logger.elideLifecycle(message: String) { + lifecycle("[ELIDE] $message") +} + + +internal fun Logger.elideDebug(message: String) { + debug("[ELIDE] $message") +} + +internal fun Logger.elideWarn(message: String) { + warn("[ELIDE] $message") +} + +internal fun Logger.elideError(message: String) { + error("[ELIDE] $message") +} diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/Platform.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/Platform.kt new file mode 100644 index 0000000..196b6e1 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/Platform.kt @@ -0,0 +1,18 @@ +package dev.elide.gradle.internal + +internal object Platform { + val platformClassifier: String get() { + val elideArch = when (System.getProperty("os.arch").lowercase()) { + "x86_64", "amd64" -> "amd64" + "arm64", "aarch64" -> "aarch64" + else -> error("Unsupported architecture: ${System.getProperty("os.arch")}") + } + + return when (System.getProperty("os.name").lowercase()) { + "linux" -> "linux-$elideArch" + "mac os x" -> "darwin-$elideArch" + "windows" -> "windows-$elideArch" + else -> error("Unsupported OS: ${System.getProperty("os.name")}") + } + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/ProviderExt.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/ProviderExt.kt new file mode 100644 index 0000000..130d998 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/ProviderExt.kt @@ -0,0 +1,19 @@ +package dev.elide.gradle.internal + +import org.gradle.api.provider.Provider + +/** + * Transforms the value of the provider lazily. + * + * Created to bypass java-interop issues with inability to pass null to an original map. We + * work around it by avoiding the generation of kotlin's intrinsics on 'as' casts using generics that are by + * specification are unchecked. + */ +internal fun Provider.mapNotNull( + transform: (TIn) -> TOut?, +): Provider { + return map { + // @ts-ignore ahhh moment + transform(it) as TOut + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/exception/ElideGenericFailureException.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/exception/ElideGenericFailureException.kt new file mode 100644 index 0000000..70af1bb --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/exception/ElideGenericFailureException.kt @@ -0,0 +1,6 @@ +package dev.elide.gradle.internal.exception + +internal class ElideGenericFailureException : Exception( + "An internal elide cli error has occurred. Check logs with either `--debug` flag or " + + "`elide.diagnostics.debug = true` for more information." +) \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/exception/ElideUserFailureException.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/exception/ElideUserFailureException.kt new file mode 100644 index 0000000..dc0fc38 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/internal/exception/ElideUserFailureException.kt @@ -0,0 +1,6 @@ +package dev.elide.gradle.internal.exception + +internal class ElideUserFailureException : Exception( + "An error in code / configuration is present. Check logs with either `--debug` flag or " + + "`elide.diagnostics.debug = true` for more information." +) \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/service/ElideThreadPoolService.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/service/ElideThreadPoolService.kt new file mode 100644 index 0000000..26b5b6d --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/service/ElideThreadPoolService.kt @@ -0,0 +1,14 @@ +package dev.elide.gradle.service + +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +internal abstract class ElideThreadPoolService : BuildService, AutoCloseable { + val executor: ExecutorService = Executors.newCachedThreadPool() + + override fun close() { + executor.shutdown() + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/ElideCheckVersionTask.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/ElideCheckVersionTask.kt new file mode 100644 index 0000000..9b23000 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/ElideCheckVersionTask.kt @@ -0,0 +1,126 @@ +package dev.elide.gradle.task + +import dev.elide.gradle.ElideGradlePlugin +import dev.elide.gradle.cli.ElideCli +import dev.elide.gradle.cli.ElideCliInvocationResult +import dev.elide.gradle.cli.getSuccessValueOrElse +import dev.elide.gradle.configuration.binary.ElideBinaryConfiguration +import dev.elide.gradle.configuration.binary.ElideBinaryResolutionSource +import dev.elide.gradle.service.ElideThreadPoolService +import dev.elide.gradle.task.exec.ElideCliExec +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.services.ServiceReference +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.isExecutable +import kotlin.io.path.notExists +import kotlin.io.path.setPosixFilePermissions + +public abstract class ElideCheckVersionTask : DefaultTask() { + + @get:Internal + internal abstract val cli: Property + + /** + * Sets path to the Elide's binary. + */ + @get:Input + internal abstract val binPath: Property + + /** + * Sets whether to ignore absence of Elide's binary. The task will still fail [strictVersionCheck] + * is enabled and versions don't match. + */ + @get:Input + public abstract val silentMode: Property + + /** + * Sets the target version of the Elide to check against. + * + * @see ElideBinaryConfiguration.version + */ + @get:Input + internal abstract val targetVersion: Property + + /** + * Sets the strategy of resolving Elide binary. If not [ElideBinaryResolutionSource.LocalOnly], + * the task is skipped. + */ + @get:Input + internal abstract val resolutionSource: Property + + /** + * Sets whether to check the version. If false, a task is skipped. + */ + @get:Input + internal abstract val strictVersionCheck: Property + + /** + * Shared thread pool service used for executing CLI invocation tasks. + */ + @get:ServiceReference(ElideGradlePlugin.THREAD_POOL_SERVICE_NAME) + internal abstract val threadPoolService: ElideThreadPoolService + + /** + * The cached resolved version of the Elide. + */ + @get:OutputFile + public abstract val versionFile: RegularFileProperty + + init { + group = "Elide" + description = "Validates the elide's binary version if strict check is enabled and it's a local-only resolution source" + + onlyIf("`elide.binary.strictVersionCheck` is set to false") { + strictVersionCheck.get() + } + onlyIf("`elide.binary.resolutionSource` is not LocalOnly or LocalIfApplicable") { + val source = resolutionSource.get() + + source is ElideBinaryResolutionSource.LocalOnly || + source is ElideBinaryResolutionSource.LocalIfApplicable + } + onlyIf("Local binary is not found and silent mode is enabled") { + Path(binPath.get()).exists() || silentMode.get() + } + } + + @TaskAction + public fun check() { + val bin = Path(binPath.get()).toRealPath() + + if (bin.notExists()) { + error("Unable to validate the version of Elide: binary does not exist at specified path: ${bin.absolutePathString()}.") + } + + if (!bin.isExecutable() && !bin.toFile().setExecutable(true)) { + error("Unable to validate the version of Elide: binary is not executable and cannot be set from the build. Try `chmod +x ${binPath.get()}`.") + } + + val version = cli.get().getVersion(threadPoolService.executor) + .getSuccessValueOrElse { failure -> + when (failure) { + is ElideCliInvocationResult.Error -> throw IllegalStateException( + "Unable to validate version of Elide", + failure.exception + ) + is ElideCliInvocationResult.ExitFailure -> error("Unable to validate version of Elide: non-zero exit. Exit code: ${failure.exitCode}") + } + } + + if (version != targetVersion.get() && resolutionSource.get() is ElideBinaryResolutionSource.LocalOnly) { + error("Elide version check failed due to version mismatch: expected ${targetVersion.get()}, but got $version.") + } + + versionFile.get().asFile.writeText(version) + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/GenerateModuleBuildConfigurationTask.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/GenerateModuleBuildConfigurationTask.kt new file mode 100644 index 0000000..565a899 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/GenerateModuleBuildConfigurationTask.kt @@ -0,0 +1,213 @@ +package dev.elide.gradle.task + +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.repositories.MavenArtifactRepository +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.net.URI +import kotlin.io.path.absolutePathString +import kotlin.io.path.toPath + +/** + * Gradle task that generates a `.pkl` file containing + * the current module's resolved Maven dependencies and repositories. + * + * This file is consumed by Elide's build tooling to enable + * Elide-native dependency resolution instead of relying solely on Gradle. + * + * The generated `.pkl` file declares: + * - a listing of Maven package dependencies (`packages`) that are linked to a source set. + * - a listing of Maven repositories (`repositories`) + * + * These constants can be imported into the Elide buildscript to + * propagate the effective dependencies and repository information, + * ensuring consistent resolution and build reproducibility. + * + * This task is optional and only required if the user opts to + * replace Gradle’s dependency resolution with Elide’s mechanism. + * + * Example usage of the generated file in `.pkl`: + * ``` + * amends "elide:project.pkl" + * + * import "build/elide-runtime/generated/generated.pkl" as gradleGenerated + * + * dependencies { + * maven { + * packages = gradleGenerated.packages + * // repositories can be set similarly + * } + * } + * ``` + */ +public abstract class GenerateModuleBuildConfigurationTask : DefaultTask() { + + init { + group = "Elide" + description = + "Generates a helper `.pkl` file for the `manifest.pkl` to propagate actual module dependencies and repositories." + } + + /** + * The list of Maven artifact repositories declared in the current module. + * These will be serialized to the generated `.pkl` to allow Elide + * to replicate repository resolution. + */ + @get:Input + public abstract val declaredRepositories: ListProperty + + /** + * The list of Maven package dependency coordinates declared in the current module for a main source set. + * These strings represent Maven coordinates such as + * `"group:name:version"`. + */ + @get:Input + public abstract val mainDeclaredDependencies: ListProperty + + /** + * The list of Maven package dependency coordinates declared in the current module for a test source set. + * These strings represent Maven coordinates such as + * `"group:name:version"`. + */ + @get:Input + public abstract val testDeclaredDependencies: ListProperty + + /** + * The output `.pkl` file that will be generated and imported by Elide's build scripts. + */ + @get:OutputFile + public abstract val generatedFile: RegularFileProperty + + @TaskAction + public fun generate() { + generatedFile.get().asFile.writeText( + text = buildString { + appendLine(GENERATION_TEMPLATE) + appendLine(dependenciesCode("main", mainDeclaredDependencies.get())) + appendLine(dependenciesCode("test", testDeclaredDependencies.get())) + appendLine(repositoriesCode(declaredRepositories.get().map { it.url })) + } + ) + } + + private companion object { + private val GENERATION_TEMPLATE = """ + /* + * This file is **auto-generated** by the Elide Gradle Plugin. + * Do NOT edit manually — changes will be overwritten on each Gradle build. + * + * This module, `gradleGenerated`, exposes two important constants: + * + * 1. `dependencies`: A Listing of Maven package dependencies resolved from your Gradle build. + * Each entry is a Maven coordinate string or structured dependency that Elide can consume. + * + * 2. `repositories`: A Listing of Maven repositories where dependencies are resolved. + * This includes remote URLs and local file-based repositories. + * + * These constants allow Elide’s build tooling to replicate Gradle’s dependency + * resolution and ensure your Elide build uses exactly the same artifacts. + * + * Usage: + * ``` + * amends "elide:project.pkl" + * + * import "build/elide-runtime/generated/module.pkl" as gradleModule + * + * dependencies { + * maven { + * packages = gradleModule.dependencies + * repositories = gradleModule.repositories + * } + * } + * ``` + * + * This seamless integration ensures that your Elide build environment + * reflects the real Gradle dependencies and repository setup. + */ + module gradleModule + + import "elide:jvm.pkl" as jvm + """.trimIndent() + + fun dependenciesCode(sourceSetName: String, coordinates: List): String { + return """ + /// List of Maven package dependencies detected from Gradle $sourceSetName's source set. + /// Each item is a `jvm.MavenPackageDependency` representing + /// an artifact coordinate or package spec. + const ${sourceSetName}Dependencies: Listing = new Listing { + ${coordinates.joinToString("\n") { "\"$it\"" }} + } + """.trimIndent() + } + + fun repositoriesCode(repos: List): String = buildString { + appendLine(""" + /// List of Maven repositories Gradle uses to resolve dependencies. + /// Includes both remote URLs and local file system repositories. + /// Each entry is a `jvm.MavenRepository` specification. + const repositories: Listing = new Listing { + """.trimIndent()) + + repos.forEach { repo -> + val isLocal = repo.scheme == "file" + val parameterName = if (isLocal) "path" else "url" + val repoName = generateRepositoryName(repo.toASCIIString()) + val value = if (isLocal) repo.toPath().absolutePathString() else repo.toURL().toString() + + appendLine("\t[\"$repoName\"] = new {") + appendLine("\t\t$parameterName = \"$value\"") + appendLine("\t}") + } + appendLine("}") + } + + private val wellKnownRepositories = mapOf( + "https://repo.maven.apache.org/maven2" to "mavenCentral", + "https://jcenter.bintray.com" to "jcenter", + "https://jitpack.io" to "jitpack", + "https://plugins.gradle.org/m2" to "gradlePluginPortal", + "https://maven.pkg.jetbrains.space/public/p/ktor/eap" to "ktorEap", + "https://s01.oss.sonatype.org/content/repositories/snapshots/" to "sonatypeSnapshots", + "https://maven.google.com" to "googleMaven", + (File(System.getProperty("user.home") + "/.m2/repository").normalize().absolutePath) to "localMaven", + ) + + private fun generateRepositoryName(pathOrUrl: String): String { + val trimmed = pathOrUrl.trimEnd('/') + + // Try to match known repo first + wellKnownRepositories.forEach { (pattern, name) -> + if (pattern.startsWith("file:") && trimmed.matches(Regex(pattern))) return name + if (pattern.startsWith("http") && trimmed.equals(pattern, ignoreCase = true)) return name + } + + val input = pathOrUrl + .removePrefix("https://") + .removePrefix("http://") + .removePrefix("file:") + .removePrefix("s3://") + .removePrefix("ftp://") + .removePrefix("ssh://") + .removePrefix("~") + .removePrefix("/") + .removePrefix("./") + .removePrefix("../") + .replace(Regex("[^a-zA-Z0-9/_\\-.]+"), "") // clean weird chars + .replace(Regex("[.]"), "/") // treat dots like slashes for domain parts + + return input + .split(Regex("[/_\\-]+")) + .filter { it.isNotBlank() } + .mapIndexed { index, token -> + val normalized = token.replace(Regex("[^a-zA-Z0-9]"), "") + if (index == 0) normalized.lowercase() + else normalized.replaceFirstChar { it.uppercase() } + } + .joinToString("") + } + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/PrepareElideJavadocShimTask.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/PrepareElideJavadocShimTask.kt new file mode 100644 index 0000000..55232ef --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/PrepareElideJavadocShimTask.kt @@ -0,0 +1,49 @@ +package dev.elide.gradle.task + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +/** + * Task that creates a shell script to replace the default `javadoc` executable with a shim + * that redirects to `elide javadoc ...`. This is used to integrate Elide's Javadoc processor + * into Gradle builds. + */ +public abstract class PrepareElideJavadocShimTask : DefaultTask() { + + init { + group = "Elide" + description = "Creates a shim script to invoke `elide javadoc` instead of the standard Javadoc tool." + } + + /** Absolute path to the Elide CLI binary. */ + @get:Input + public abstract val cliPath: Property + + /** Location where the shim script will be written. */ + @get:OutputFile + public abstract val shimFile: RegularFileProperty + + @TaskAction + public fun generateShim() { + val shimFile = shimFile.get().asFile + val cli = cliPath.get() + + val script = """ + #!/usr/bin/env bash + exec "$cli" javadoc "$@" + """.trimIndent() + + shimFile.parentFile.mkdirs() + shimFile.writeText(script) + + if (!shimFile.setExecutable(true)) { + throw IllegalStateException( + "Unable to set executable bit on shim script at ${shimFile.absolutePath}" + ) + } + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/DownloadElideBinaryTask.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/DownloadElideBinaryTask.kt new file mode 100644 index 0000000..88a7786 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/DownloadElideBinaryTask.kt @@ -0,0 +1,108 @@ +package dev.elide.gradle.task.download + +import de.undercouch.gradle.tasks.download.Download +import dev.elide.gradle.cli.ElideCli +import dev.elide.gradle.configuration.binary.ElideBinaryConfiguration +import dev.elide.gradle.configuration.binary.ElideBinaryResolutionSource +import dev.elide.gradle.internal.Platform +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal + +public abstract class DownloadElideBinaryTask : Download() { + + /** + * The strategy used to resolve the Elide binary source. + * Controls whether the binary should be downloaded, used locally, or other resolution methods. + * + * @see ElideBinaryConfiguration.resolutionSource + */ + @get:Input + public abstract val resolutionSource: Property + + /** + * The target version of the Elide binary to download. + * Changing this will invalidate the cache and trigger re-download. + * + * @see ElideBinaryConfiguration.version + */ + @get:Input + public abstract val targetVersion: Property + + /** + * The directory where the downloaded files (archive and signature) will be saved. + * This directory is cleared before each download attempt. + * + * By default, [targetDirectory] is the defined download path in [ElideBinaryResolutionSource] + [targetVersion] + * + * @see ElideBinaryResolutionSource + */ + @get:Internal + public abstract val targetDirectory: DirectoryProperty + + /** + * The output file representing the downloaded Elide binary archive (`elide.zip`). + * Declared as output so Gradle can track task up-to-date checks properly. + * + * @see DownloadElideBinaryTask.archiveFile + */ + @get:Internal + protected val archiveFile: Property = targetDirectory.file("elide.zip") as Property + + /** + * The output file representing the signature bundle for the Elide binary archive (`elide.zip.sigstore`). + * Declared as output for Gradle incremental build support. + * + * @see DownloadElideBinaryTask.sigstoreFile + */ + @get:Internal + protected val sigstoreFile: Property = targetDirectory.file("elide.zip.sigstore") as Property + + init { + group = "Elide" + + // we assume nothing changes on another side, checking hash every build is quite + // expensive, so we want to avoid it + outputs.upToDateWhen { + archiveFile.get().asFile.exists() && sigstoreFile.get().asFile.exists() + } + + onlyIf("resolutionSource is not defined") { + resolutionSource.orNull != null + } + onlyIf("resolutionSource is local-only") { + resolutionSource.orNull !is ElideBinaryResolutionSource.LocalOnly + } + onlyIf("resolutionSource is prefer-local and local version is matched") { + ElideCli.resolvePathToCli(logger, project.providers).isPresent + } + onlyIf("`elide.binary.version` is undefined") { + targetVersion.orNull != null + } + + doFirst { + targetDirectory.get().asFileTree.forEach { it.delete() } + } + + val osClassifier = Platform.platformClassifier + + src( + listOf( + "https://elide.zip/cli/v1/snapshot/${osClassifier}/${targetVersion.get()}/elide.zip", + "https://github.com/elide-dev/elide/releases/download/${targetVersion.get()}/elide-${targetVersion.get()}-$osClassifier.zip.sigstore" + ) + ) + dest(targetDirectory) + overwrite(false) + + eachFile { + path = when { + sourceURL.path.endsWith(".sigstore") -> sigstoreFile.get().asFile.absolutePath + sourceURL.path.endsWith(".zip") -> archiveFile.get().asFile.absolutePath + else -> error("Should not reach this state; Expected either .sigstore or .zip files to be downloaded.") + } + } + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/ExtractElideBinaryTask.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/ExtractElideBinaryTask.kt new file mode 100644 index 0000000..9bdaa60 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/ExtractElideBinaryTask.kt @@ -0,0 +1,52 @@ +package dev.elide.gradle.task.download + +import dev.elide.gradle.configuration.binary.ElideBinaryResolutionSource +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import kotlin.io.path.deleteIfExists + +public abstract class ExtractElideBinaryTask : Copy() { + /** + * The directory where the downloaded files (archive and signature) will be saved. + * This directory is cleared before each download attempt. + * + * @see ElideBinaryResolutionSource + */ + @get:OutputDirectory + public abstract val targetDirectory: DirectoryProperty + + /** + * The output file representing the downloaded Elide binary archive (`elide.zip`). + * Declared as internal not to compute hashes every time. + * + * @see DownloadElideBinaryTask.archiveFile + */ + @get:Internal + private val archiveFile: Property = targetDirectory.file("elide.zip") as Property + + /** + * The output file representing the signature bundle for the Elide binary archive (`elide.zip.sigstore`). + * Declared as output for Gradle incremental build support. + * + * @see DownloadElideBinaryTask.sigstoreFile + */ + @get:Internal + private val sigstoreFile: Property = + targetDirectory.file("elide.zip.sigstore") as Property + + init { + onlyIf("Archive file does not exist") { archiveFile.get().asFile.exists() } + + val archiveFile = archiveFile.get().asFile.toPath() + + from(archiveFile) + into(targetDirectory) + + doLast { + archiveFile.deleteIfExists() + sigstoreFile.get().asFile.delete() + } + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/PrepareDownloadedElideBinary.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/PrepareDownloadedElideBinary.kt new file mode 100644 index 0000000..4405997 --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/PrepareDownloadedElideBinary.kt @@ -0,0 +1,57 @@ +package dev.elide.gradle.task.download + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault +import java.nio.file.attribute.PosixFilePermission +import kotlin.io.path.absolutePathString +import kotlin.io.path.isExecutable +import kotlin.io.path.setPosixFilePermissions + +@DisableCachingByDefault(because = "Computing inputs are heavier than checking file on executability.") +public abstract class PrepareDownloadedElideBinary : DefaultTask() { + init { + group = "Elide" + description = "Sets executable flag on the downloaded binary" + } + + private companion object { + val binPerms: Set = setOf( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_EXECUTE + ) + } + /** + * Downloaded and extracted Elide's binary. + * + * Internal is because computing inputs are heavier than a checking file on executability and actually setting it. + * + * @see ExtractElideBinaryTask + */ + @get:Internal + public abstract val downloadedElideBinary: RegularFileProperty + + init { + onlyIf("Downloaded binary is not found.") { downloadedElideBinary.asFile.get().exists() } + onlyIf("Downloaded binary is already executable.") { !downloadedElideBinary.asFile.get().toPath().isExecutable() } + } + + @TaskAction + public fun prepare() { + val file = downloadedElideBinary.asFile.get().toPath() + + try { + if (!System.getProperty("os.name").contains("windows", ignoreCase = true)) + file.setPosixFilePermissions(binPerms) + } catch (e: Exception) { + throw IllegalStateException("Unable to make ${file.absolutePathString()} executable; try `sudo chmod +x ${file.absolutePathString()}` yourself.", e) + } + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/VerifyElideBinaryTask.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/VerifyElideBinaryTask.kt new file mode 100644 index 0000000..e61d83a --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/download/VerifyElideBinaryTask.kt @@ -0,0 +1,83 @@ +package dev.elide.gradle.task.download + +import dev.elide.gradle.configuration.binary.ElideBinaryResolutionSource +import dev.sigstore.KeylessVerificationException +import dev.sigstore.KeylessVerifier +import dev.sigstore.VerificationOptions +import dev.sigstore.bundle.Bundle +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +public abstract class VerifyElideBinaryTask : DefaultTask() { + + init { + group = "Elide" + description = "Verifies the downloaded by `downloadElideCli` task archive's signature." + } + + /** + * The directory where the downloaded files (archive and signature) will be saved. + * This directory is cleared before each download attempt. + * + * @see ElideBinaryResolutionSource + */ + @get:Internal + public abstract val targetDirectory: DirectoryProperty + + /** + * The output file representing the downloaded Elide binary archive (`elide.zip`). + * Declared as output so Gradle can track task up-to-date checks properly. + * + * @see DownloadElideBinaryTask.archiveFile + */ + @get:Internal + @get:PathSensitive(PathSensitivity.RELATIVE) + private val archiveFile: Property = targetDirectory.file("elide.zip") as Property + + /** + * The output file representing the signature bundle for the Elide binary archive (`elide.zip.sigstore`). + * Declared as output for Gradle incremental build support. + * + * @see DownloadElideBinaryTask.sigstoreFile + */ + @get:Internal + @get:PathSensitive(PathSensitivity.RELATIVE) + private val sigstoreFile: Property = + targetDirectory.file("elide.zip.sigstore") as Property + + init { + onlyIf("archive file and sigstore file is not present") { archiveFile.get().asFile.exists() && sigstoreFile.get().asFile.exists() } + } + + @TaskAction + public fun verify() { + val bundle = Bundle.from(sigstoreFile.get().asFile.toPath(), Charsets.UTF_8) + val verifier = KeylessVerifier.builder() + .sigstorePublicDefaults() + .build() + + try { + verifier.verify( + archiveFile.get().asFile.toPath(), + bundle, + VerificationOptions.builder().build() + ) + } catch (e: KeylessVerificationException) { + archiveFile.get().asFile.delete() + sigstoreFile.get().asFile.delete() + // clean up invalid archive and .sigstore + throw IllegalStateException( + "Downloaded `${archiveFile.get().asFile.absolutePath}` failed to be verified", + e + ) + } + + sigstoreFile.get().asFile.delete() + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/exec/ElideCliExec.kt b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/exec/ElideCliExec.kt new file mode 100644 index 0000000..f4877ea --- /dev/null +++ b/elide-gradle-plugin/src/main/kotlin/dev/elide/gradle/task/exec/ElideCliExec.kt @@ -0,0 +1,181 @@ +package dev.elide.gradle.task.exec + +import dev.elide.gradle.ElideGradlePlugin +import dev.elide.gradle.cli.ElideCli +import dev.elide.gradle.cli.ElideCliInvocation +import dev.elide.gradle.internal.elideDebug +import dev.elide.gradle.internal.elideError +import dev.elide.gradle.service.ElideThreadPoolService +import org.gradle.api.DefaultTask +import org.gradle.api.Transformer +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.services.ServiceReference +import org.gradle.api.tasks.Console +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.isExecutable + +/** + * Gradle task for executing the Elide CLI with configured arguments, + * debug and verbose flags, and capturing output streams for logging. + * + * Supports optional transformation of the underlying [ElideCliInvocation] + * to customize invocation behavior or result handling. + */ +public abstract class ElideCliExec : DefaultTask() { + /** + * Sets whether to ignore absence of Elide's binary and do nothing in that case. + */ + @get:Input + public abstract val silentMode: Property + + /** + * Sets path to the Elide's binary. + */ + @get:Input + internal abstract val binPath: Property + + /** + * The [ElideCli] instance used to create CLI invocations. + */ + @get:Internal + public abstract val cli: Property + + /** + * Enables or disables debug mode for the Elide CLI binary. + * By default, uses the debug flag from the Elide diagnostics configuration. + */ + @get:Internal + @get:Console + public abstract val debug: Property + + /** + * Enables or disables verbose mode for the Elide CLI binary. + * By default, uses the verbose flag from the Elide diagnostics configuration. + */ + @get:Internal + @get:Console + public abstract val verbose: Property + + /** + * Enables or disables telemetry used in Elide. + * + * @see dev.elide.gradle.configuration.ElideDiagnosticsConfiguration.telemetry + */ + @get:Internal + @get:Console + public abstract val telemetry: Property + + /** + * Arguments passed to the Elide CLI invocation. + */ + @get:Input + public abstract val args: ListProperty + + /** + * Shared thread pool service used for executing CLI invocation tasks. + */ + @get:ServiceReference(ElideGradlePlugin.THREAD_POOL_SERVICE_NAME) + internal abstract val threadPoolService: ElideThreadPoolService + + @get:Internal + private var invocationTransformer: Transformer, ElideCliInvocation<*>>? = null + + /** + * Sets the output file to write a result. Unless you write something to it, + * a task will rerun on every build. + */ + @get:OutputFile + public abstract val outputFile: RegularFileProperty + + /** + * Sets an optional transformer to customize the [ElideCliInvocation]. + * + * If specified, this transformer will be applied to the invocation + * before execution, allowing users to modify logging, output handling, or other behaviors. + * + * @param transformer a [Transformer] that maps an invocation to a new invocation. + */ + public fun useInvocation(transformer: Transformer, ElideCliInvocation<*>>? = null) { + invocationTransformer = transformer + } + + /** + * Adds arguments to the invocation. + * + * @param args vararg list of CLI arguments. + */ + public fun args(vararg args: String) { + this.args.addAll(args.toList()) + } + + init { + outputs.upToDateWhen { + val file = outputFile.get().asFile + file.exists() && file.length() != 0L + } + + // Skip task execution if no arguments are specified. + onlyIf("no arguments specified") { + args.getOrElse(emptyList()).isNotEmpty() + } + + onlyIf("Elide binary is not resolvable or non-executable; skipping due to silent mode enabled") { + val path = Path(binPath.get()).toRealPath() + path.exists() && path.isExecutable() && silentMode.get() + } + } + + /** + * Task action that creates and executes the Elide CLI invocation + * with the configured arguments, debug/verbose flags, and thread pool. + * + * If an invocation transformer is set via [useInvocation], applies it before executing. + * Otherwise, logs stdout and stderr live, and fails on non-success exit codes or exceptions. + */ + @TaskAction + public fun execute() { + val invocation = cli.get().createInvocation( + buildList { + if (debug.getOrElse(false)) + add("--debug") + + if (verbose.getOrElse(false)) + add("--verbose") + + addAll(args.getOrElse(emptyList()).filter { it.isNullOrEmpty() }) + }, + threadPoolService.executor, + ).let { invocation -> + if (invocationTransformer == null) { + invocation + .consumeStdout { stream -> + stream.forEach { + logger.elideDebug(it) + } + }.consumeStderr { stream -> + stream.forEach { + logger.elideError(it) + } + } + .onNonZeroExitCode { exitCode -> + error("Failed to invoke `elide ${args.get().joinToString(" ")}`. Process finished with non-success exit code: $exitCode") + }.onException { exception -> + throw IllegalStateException("Failed to invoke `elide ${args.get().joinToString(" ")}`", exception) + } + } else { + invocationTransformer!!.transform(invocation) + } + } + + outputFile.get().asFile.createNewFile() + + invocation.execute() + } +} \ No newline at end of file diff --git a/elide-gradle-plugin/src/test/java/com/example/plugin/ElidePluginTest.java b/elide-gradle-plugin/src/test/java/com/example/plugin/ElidePluginTest.java deleted file mode 100644 index 363b0df..0000000 --- a/elide-gradle-plugin/src/test/java/com/example/plugin/ElidePluginTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.plugin; - -import org.gradle.testfixtures.ProjectBuilder; -import org.gradle.api.Project; -import org.junit.Test; -import static org.junit.Assert.assertNotNull; - - -public class ElidePluginTest { - @Test - public void pluginDoesntFailTheBuild() { - // Create a test project and apply the plugin - Project project = ProjectBuilder.builder().build(); - project.getPlugins().apply("dev.elide"); - - // Verify the result - // assertNotNull(project.getTasks().findByName("tasks")); - } -} diff --git a/gradle.properties b/gradle.properties index 24f0553..aef125e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1 @@ version=1.0.0 -elide.version=1.0.0-beta5 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..7c7b785 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,29 @@ +[versions] +kotlin = "2.0.21" +gradle-plugin-publish = "1.3.1" +undercouch-download = "5.6.0" +sigstore = "1.3.0" + +[libraries] +# ───────────────────────────────────────────── +# Test Libraries +# ───────────────────────────────────────────── +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } + +# ───────────────────────────────────────────── +# Plugin Development Dependencies +# (used when implementing Gradle plugins) +# ───────────────────────────────────────────── +pluginClasspath-undercouch-download = { module = "de.undercouch.download:de.undercouch.download.gradle.plugin", version.ref = "undercouch-download" } +pluginClasspath-kotlin = { module = "org.jetbrains.kotlin.android:org.jetbrains.kotlin.android.gradle.plugin", version.ref = "kotlin" } + +# ───────────────────────────────────────────── +# Security & Signing Libraries +# ───────────────────────────────────────────── +sigstore = { module = "dev.sigstore:sigstore-java", version.ref = "sigstore" } + +[plugins] +gradle-publish = { id = "com.gradle.plugin-publish", version.ref = "gradle-plugin-publish" } +undercouch-download = { id = "de.undercouch.download", version.ref = "undercouch-download" } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca025c8..ff23a68 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 34ff31e..997496c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,4 +3,12 @@ rootProject.name = "elide-gradle" include("elide-gradle-plugin") include("elide-gradle-catalog") -includeBuild("example-project") +dependencyResolutionManagement { + this.repositoriesMode +} + +pluginManagement { + +} + +//includeBuild("example-project")