Skip to content

Commit

Permalink
Introduce API distinction in plugin development
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Lacasse <[email protected]>
  • Loading branch information
lacasseio committed Jul 10, 2024
1 parent ace30d4 commit adc8a0b
Show file tree
Hide file tree
Showing 13 changed files with 1,217 additions and 19 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dev.gradleplugins.testers;

import dev.gradleplugins.buildscript.GradleDsl;
import dev.gradleplugins.buildscript.ast.ExpressionBuilder;
import dev.gradleplugins.buildscript.ast.expressions.Expression;
import dev.gradleplugins.buildscript.io.GradleBuildFile;
import dev.gradleplugins.runnerkit.GradleRunner;

import static dev.gradleplugins.buildscript.blocks.GradleBuildScriptBlocks.afterEvaluate;
import static dev.gradleplugins.buildscript.syntax.Syntax.groovyDsl;
import static dev.gradleplugins.buildscript.syntax.Syntax.string;

public interface DependencyWiringTester {
ExpressionBuilder<?> bucketDsl();

GradleBuildFile buildFile();

GradleRunner runner();

default void assertBucketDependencyIs(Expression expression) {
buildFile().append(afterEvaluate(it -> it.add(bucketDsl().call(string("com.example:foo:1.0")))));
buildFile().append(groovyDsl(
"Set<String> allDependencies(Configuration configuration) {",
" return configuration.allDependencies.collect {",
" if (it instanceof ExternalModuleDependency) {",
" return \"${it.group}:${it.name}:${it.version}\".toString()",
" } else if (it instanceof ProjectDependency) {",
" return it.projectPath",
" } else if (it instanceof SelfResolvingDependency) {",
" return it.targetComponentId?.displayName ?: '<no-display-name>'",
" } else {",
" throw new RuntimeException()",
" }",
" }",
"}"
));
buildFile().append(groovyDsl(
"tasks.register('verify') {",
" doLast {",
" assert " + expression.toString(GradleDsl.GROOVY),
" }",
"}"
));

runner().withTasks("verify").build();
}

default Expression containedIn(Expression configurationName) {
return groovyDsl("allDependencies(configurations[" + configurationName.toString(GradleDsl.GROOVY) + "]).contains('com.example:foo:1.0')");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package dev.gradleplugins.testers;

import dev.gradleplugins.buildscript.io.GradleBuildFile;
import dev.gradleplugins.buildscript.io.GradleSettingsFile;
import dev.gradleplugins.fixtures.sample.GradlePluginApi;
import dev.gradleplugins.runnerkit.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.nio.file.Path;

import static dev.gradleplugins.buildscript.syntax.Syntax.groovyDsl;

public abstract class GradlePluginApiTester {
GradleBuildFile pluginBuildFile;

public abstract GradleRunner runner();

public abstract Path testDirectory();

public final GradleBuildFile pluginBuildFile() {
return pluginBuildFile;
}

public abstract GradlePluginApi fixture();

@BeforeEach
final void givenProject() {
pluginBuildFile = GradleBuildFile.inDirectory(testDirectory());

fixture().writeToProject(testDirectory().toFile());

pluginBuildFile.append(groovyDsl(
"group = 'com.example'",
"version = '1.0'",
"",
"repositories {",
" gradlePluginDevelopment()",
"}",
"",
"gradlePlugin {",
" plugins {",
" basic {",
" id = '" + fixture().getPluginId() + "'",
" implementationClass = 'com.example.BasicPlugin'",
" }",
" }",
"}",
"",
"Set<File> artifactFiles(def publications) {",
" return publications.artifacts.files.files",
"}"
));
GradleSettingsFile.inDirectory(testDirectory());
}

@Test
void rewiresApiElementsToIncludeOnly__ApiJar__ApiClasses() {
pluginBuildFile.append(groovyDsl(
"tasks.register('verify') {",
" doLast {",
" assert artifactFiles(configurations.apiElements.outgoing) == [file('build/libs/my-plugin-1.0-api.jar')] as Set",
" assert artifactFiles(configurations.apiElements.outgoing.variants.classes) == [file('build/tmp/syncApiClasses')] as Set",
" }",
"}"
));

runner().withTasks("verify").build();
}

// TODO: If we use a custom pluginSourceSet -> the runtimeElements and apiElements won't contain any outgoing information...

@Test
void rewiresRuntimeElementsToInclude__ApiAndMainJar__ApiAndMainClasses__ApiAndMainResources() {
pluginBuildFile.append(groovyDsl(
"tasks.register('verify') {",
" doLast {",
" assert artifactFiles(configurations.runtimeElements.outgoing) == [file('build/libs/my-plugin-1.0.jar'), file('build/libs/my-plugin-1.0-api.jar')] as Set",
" assert artifactFiles(configurations.runtimeElements.outgoing.variants.classes) == [file('build/classes/java/main'), file('build/tmp/syncApiClasses')] as Set",
" assert artifactFiles(configurations.runtimeElements.outgoing.variants.resources) == [file('build/resources/main'), file('build/tmp/syncApiResources')] as Set",
" }",
"}"
));

runner().withTasks("verify").build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dev.gradleplugins;

import org.gradle.api.plugins.ExtensionAware;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.plugin.devel.GradlePluginDevelopmentExtension;

import java.util.Objects;

/**
* An extension for controlling the Gradle plugin development API.
* The extension is registered on the {@literal gradlePlugin} extension.
* The API refers to what a user can build against, e.g. some classes are for internal use only.
* This extension offers some hook points to accomplish the separation at the source set or Jar task level.
*/
public interface GradlePluginDevelopmentApiExtension {
/**
* Configures the API source set for the plugin.
*
* <p>
* <code>
* // Configure Gradle plugin API as a custom source set
* gradlePlugin {
* api {
* sourceSet = sourceSets.register('api')
* }
* }
* </code>
* </p>
*
* @return a property to configure the API source set, never null.
*/
Property<SourceSet> getSourceSet();

/**
* Returns the task provider for the exported API JAR.
*
* Note: Once this property is queried, it's not possible to change the Jar task anymore.
*
* @return a task provider to the exported API JAR, never null
*/
TaskProvider<Jar> getJarTask();

/**
* Configures the API Jar for the plugin.
*
* <p>
* <code>
* // Remove `api` packages from implementation JAR
* tasks.named('jar', Jar) {
* exclude('**\/api\/**')
* }
*
* // Configure Gradle plugin API as a custom JAR that only include `api` packages
* gradlePlugin {
* api {
* jarTask = tasks.register('apiJar', Jar) {
* from(sourceSet.flatMap { it.output.elements })
* include('**\/api\/**')
* archiveClassifier = 'api'
* }
* }
* }
* </code>
* </p>
*
* @param jarTaskProvider a JAR task to use as the exported API JAR, never null.
*/
void setJarTask(TaskProvider<Jar> jarTaskProvider);

/**
* Returns {@literal api} extension from Gradle plugin development extension.
* The plugin {@literal dev.gradleplugins.gradle-plugin-base} registers this extension.
*
* @param extension the {@literal gradlePlugin} extension, must not be null
* @return the compatibility extension, never null
*/
static GradlePluginDevelopmentApiExtension api(GradlePluginDevelopmentExtension extension) {
Objects.requireNonNull(extension);
return (GradlePluginDevelopmentApiExtension) ((ExtensionAware) extension).getExtensions().getByName("api");
}
}
Loading

0 comments on commit adc8a0b

Please sign in to comment.