How to apply other plugins in custom plugin correctly

I’m trying to make a plugin, which include other three plugin java, scala, kotlin-jvm. And I hope that users can configure it in the build.gradle.kts, to determine whether to enable scala or kotlin-jvm. Here is my code.

public class JvmAIOPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        JvmParameters parameters = project.getExtensions().create("jvmAio", JvmParameters.class);
        if (parameters.getJavaVersion().isPresent()) {
            configureJava(project, parameters);
        }

        if (parameters.getKotlinVersion().isPresent() && parameters.getScalaVersion().isPresent()) {
            throw new IllegalArgumentException("do not configure kotlin and scala same time");
        } else if (parameters.getKotlinVersion().isPresent()) {
            configureKotlin(project, parameters);
        } else if (parameters.getScalaVersion().isPresent()) {
            configureScala(project, parameters);
        }
    }

    private void configureJava(Project project, JvmParameters parameters) {
        project.getPluginManager().apply(JavaPlugin.class);
        // configure javaCompile and jacoco, test tasks
        TaskContainer tasks = project.getTasks();
        tasks.withType(JavaCompile.class, javaCompile -> {
            JavaVersion javaVersion = parameters.getJavaVersion().get();
            javaCompile.setSourceCompatibility(javaVersion.name());
            javaCompile.setTargetCompatibility(javaVersion.name());
            CompileOptions options = javaCompile.getOptions();
            options.setEncoding("UTF-8");
            options.setFork(true);
            options.setIncremental(true);
            ForkOptions forkOptions = options.getForkOptions();
            forkOptions.setMemoryMaximumSize("2g");
            forkOptions.setJvmArgs(Collections.singletonList("-XX:MaxMetaspaceSize=1g"));
        });
        tasks.withType(Test.class, test -> {
            tasks.withType(JacocoReport.class, test::finalizedBy);
            test.useJUnitPlatform();
        });
        tasks.withType(JacocoReport.class, report -> {
            tasks.withType(Test.class, it -> it.dependsOn(report));
            JacocoReportsContainer reports = report.getReports();
            Path buildDirectory = project.getBuildDir().toPath();
            reports.getXml().getOutputLocation().fileValue(buildDirectory.resolve("jacocoXml.xml").toFile());
            reports.getHtml().getOutputLocation().fileValue(buildDirectory.resolve("jacocoHtml").toFile());
            reports.getCsv().getOutputLocation().fileValue(buildDirectory.resolve("jacocoCsv.csv").toFile());
        });
    }

    private void configureKotlin(Project project, JvmParameters parameters) {
        KotlinVersion kotlinVersion = parameters.getKotlinVersion().get();
        project.getPluginManager().apply("org.jetbrains.kotlin.jvm");
        if (kotlinVersion.isUseJpa()) project.getPluginManager().apply("org.jetbrains.kotlin.plugin.jpa");
        if (kotlinVersion.isUseSpring()) project.getPluginManager().apply("org.jetbrains.kotlin.plugin.spring");
        // configure kotlin tasks
        project.getTasks().withType(KotlinJvmCompile.class, kotlinCompile -> {
            KotlinJvmOptions kotlinOptions = kotlinCompile.getKotlinOptions();
            JavaVersion javaVersion = parameters.getJavaVersion().get();
            kotlinOptions.setJvmTarget(javaVersion.getMajorVersion());
            kotlinOptions.setApiVersion(kotlinVersion.majorMinor());
            kotlinOptions.setFreeCompilerArgs(Collections.singletonList("-Xjsr305=stric"));
            if (kotlinVersion.isUseK2()) kotlinOptions.setLanguageVersion("2.0");
            KotlinJvmCompilerOptions options = kotlinOptions.getOptions();
            options.getOptIn().add("kotlin.RequiresOptIn");
        });
        // configure kotlin dependency
        String library = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:" + kotlinVersion;
        String reflect = "org.jetbrains.kotlin:kotlin-reflect:" + kotlinVersion;
        DependencyHandler dependencies = project.getDependencies();
        dependencies.add("implementation", library);
        dependencies.add("implementation", reflect);
    }

    private void configureScala(Project project, JvmParameters parameters) {
        project.getPluginManager().apply(ScalaPlugin.class);
        // configure scala tasks
        ScalaVersion scalaVersion = parameters.getScalaVersion().get();
        project.getTasks().withType(ScalaCompile.class, scalaCompile -> {
            JavaVersion javaVersion = parameters.getJavaVersion().get();
            scalaCompile.setSourceCompatibility(javaVersion.name());
            scalaCompile.setTargetCompatibility(javaVersion.name());
            ScalaCompileOptions options = scalaCompile.getScalaCompileOptions();
            options.setEncoding("UTF-8");
            ScalaForkOptions forkOptions = options.getForkOptions();
            forkOptions.setMemoryMaximumSize("2g");
            forkOptions.setJvmArgs(Collections.singletonList("-XX:MaxMetaspaceSize=1g"));
        });
        // configure scala dependency
        String library = "org.scala-lang:scala-library:" + scalaVersion;
        String reflect = "org.scala-lang:scala-reflect:" + scalaVersion;
        DependencyHandler dependencies = project.getDependencies();
        dependencies.add("implementation", library);
        if (scalaVersion.getMajor() == 2) dependencies.add("implementation", reflect);
        // configure scala source set
        if (scalaVersion.isUseScalaOnly()) {
            project.getLogger().info(project.getName() + " use scala only, adjusting source set");
            SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
            configureScalaSourceDirs(sourceSets.getByName("main"));
            configureScalaSourceDirs(sourceSets.getByName("test"));
        }
    }

    private static void configureScalaSourceDirs(SourceSet main) {
        SourceDirectorySet java = main.getJava();
        ScalaSourceDirectorySet scala = main.getExtensions().getByType(ScalaSourceDirectorySet.class);
        java.getSrcDirs().forEach(scala::srcDir);
        java.setSrcDirs(Collections.emptySet());
    }
}

And I used this plugin in allprojects like

 jvmAio {
    useJava11()
    when {
        name.endsWith("-kotlin")  -> useKotlin {
            isUseJpa = true
            isUseSpring = true
        }
        name.endsWith("-scala") -> useScala {
            scalaVersion = ScalaVersion.V2_13
            isUseScalaOnly = true
        }
    }
}

tasks {
    test {
        exclude("**/*IT.*")
    }
}

But it says "test" not found, which implies that the java plugin never applied.

I copied this code pluginsManagers.apply() from Gradle’s ScalaBasePlugin.

“Maybe I am using another wrong way” I thought

The problem is, that you register the extension and then right away evaluate the properties without giving the consumer build script any chance to configure your extension.
The consumer configuration is then done after your plugin was applied and does not have any effect anymore.
The better way in your case is to have a method in your extension, that does the configuration instead of doing it in the apply method.

I see in your example that your extensions already have methods for the consumers, so instead of just setting the property in those methods, do the intended configuration in the implementation of these methods.

Btw. using allprojects { ... } or subprojects { ... } is highly discouraged for various reasons. Your JvmAIOPlugin is a convention plugin, yes, but by applying / configuring it through allprojects { ... } you make most of the advantages of convention plugins void again. :slight_smile:

Yes, my mate, my lifesaver. I realised that apply always configure immediately, and configurations following after the apply block, will not have any effect to the previous plugin.

So that, if I configure the plugin first, it will not use the settings in scripts. But if I configure the plugin after, the scripts will throw an exception, becuase the extension not register yet. So, I have to configure all java, scala and kotlin plugins in every project using my plugin. Then there should be configures and no more exceptions, since I’ve already applied what it needs.

“But I can disable the compile task”, I thought, “Then it will looks like nothing applied exactly”


As for configuring setting blocks in the script. I found there is a simpler way to achieve this. I can use afterEvaluate and run my code inside it.

You can, kind of, but you definitely shouldn’t.
afterEvaluate is legacy, bad practice, and highly discouraged except for edge cases.

afterEvaluate is almost always just a symptom treatment and defers the problem to a later and much harder to debug and reproduce point in time. That’s like using SwingUtilities.invokeLater or Platform.runLater to “fix” a GUI problem.

The main thing you do by using afterEvaluate is introducing timing problems and race conditions. What if someone does the configuration in an afterEvaluate block for example.

What a suprise, I never thought there will be performence issues. Thanks mate, that helps a lot.

It’s not performance issues, but correctness issues and race conditions that will be much harder to debug and find.