diff --git a/.github/workflows/graalvm-latest.yml b/.github/workflows/graalvm-latest.yml
index e3053b0..a1582b5 100644
--- a/.github/workflows/graalvm-latest.yml
+++ b/.github/workflows/graalvm-latest.yml
@@ -8,11 +8,11 @@ on:
push:
branches:
- master
- - '[1-9]+.[0-9]+.x'
+ - '[0-9]+.[0-9]+.x'
pull_request:
branches:
- master
- - '[1-9]+.[0-9]+.x'
+ - '[0-9]+.[0-9]+.x'
jobs:
build_matrix:
if: github.repository != 'micronaut-projects/micronaut-project-template'
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 91016fd..afca08e 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -8,11 +8,11 @@ on:
push:
branches:
- master
- - '[1-9]+.[0-9]+.x'
+ - '[0-9]+.[0-9]+.x'
pull_request:
branches:
- master
- - '[1-9]+.[0-9]+.x'
+ - '[0-9]+.[0-9]+.x'
jobs:
build:
if: github.repository != 'micronaut-projects/micronaut-project-template'
@@ -33,13 +33,13 @@ jobs:
OSS_INDEX_USERNAME: ${{ secrets.OSS_INDEX_USERNAME }}
OSS_INDEX_PASSWORD: ${{ secrets.OSS_INDEX_PASSWORD }}
steps:
- # https://github.com/actions/virtual-environments/issues/709
+ # https://github.com/actions/virtual-environments/issues/709
- name: "🗑 Free disk space"
run: |
- sudo rm -rf "/usr/local/share/boost"
- sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- sudo apt-get clean
- df -h
+ sudo rm -rf "/usr/local/share/boost"
+ sudo rm -rf "$AGENT_TOOLSDIRECTORY"
+ sudo apt-get clean
+ df -h
- name: "📥 Checkout repository"
uses: actions/checkout@v4
diff --git a/README.md b/README.md
index 7c10fb5..96a2b10 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
# Micronaut build-plugin-sourcegen
-[](https://search.maven.org/search?q=g:%22io.micronaut.project-template%22%20AND%20a:%22micronaut-project-template%22)
-[](https://github.com/micronaut-projects/micronaut-project-template/actions)
-[](https://sonarcloud.io/summary/new_code?id=micronaut-projects_micronaut-template)
+[](https://search.maven.org/search?q=g:%22io.micronaut.build.plugin.sourcegen%22%20AND%20a:%22micronaut-build-plugin-sourcegen%22)
+[](https://github.com/micronaut-projects/micronaut-build-plugin-sourcegen/actions)
+[](https://sonarcloud.io/summary/new_code?id=micronaut-projects_micronaut-build-plugin-sourcegen)
[](https://ge.micronaut.io/scans)
Micronaut build-plugin-sourcegen
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 6784052..4675969 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -1,3 +1,11 @@
plugins {
id 'groovy-gradle-plugin'
}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation(libs.sonatype.scan)
+}
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
new file mode 100644
index 0000000..6f31e6e
--- /dev/null
+++ b/buildSrc/settings.gradle
@@ -0,0 +1,7 @@
+dependencyResolutionManagement {
+ versionCatalogs {
+ libs {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-module.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-module.gradle
index 4605dad..cd679a3 100644
--- a/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-module.gradle
+++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-module.gradle
@@ -1,4 +1,28 @@
plugins {
id 'io.micronaut.build.internal.build-plugin-sourcegen-base'
id "io.micronaut.build.internal.module"
+ id "org.sonatype.gradle.plugins.scan"
}
+
+repositories {
+ maven {
+ setUrl("https://repo.gradle.org/gradle/libs-releases")
+ }
+}
+
+micronautBuild {
+ binaryCompatibility {
+ enabledAfter("1.0.0")
+ }
+}
+
+String ossIndexUsername = System.getenv("OSS_INDEX_USERNAME") ?: project.properties["ossIndexUsername"]
+String ossIndexPassword = System.getenv("OSS_INDEX_PASSWORD") ?: project.properties["ossIndexPassword"]
+boolean sonatypePluginConfigured = ossIndexUsername != null && ossIndexPassword != null
+if (sonatypePluginConfigured) {
+ ossIndexAudit {
+ username = ossIndexUsername
+ password = ossIndexPassword
+ }
+}
+
diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-testsuite.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-testsuite.gradle
new file mode 100644
index 0000000..495b8be
--- /dev/null
+++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.build-plugin-sourcegen-testsuite.gradle
@@ -0,0 +1,18 @@
+plugins {
+ id("io.micronaut.build.internal.build-plugin-sourcegen-base")
+ id("io.micronaut.build.internal.common")
+}
+
+repositories {
+ maven {
+ setUrl("https://repo.gradle.org/gradle/libs-releases")
+ }
+}
+
+tasks.withType(Test).configureEach {
+ useJUnitPlatform()
+}
+
+tasks.withType(Checkstyle).configureEach {
+ enabled = false
+}
diff --git a/config/spotless.license.java b/config/spotless.license.java
index 91ec22a..7d445b0 100644
--- a/config/spotless.license.java
+++ b/config/spotless.license.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-$YEAR original authors
+ * Copyright 2025 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -12,4 +12,4 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
- */
\ No newline at end of file
+ */
diff --git a/gradle.properties b/gradle.properties
index 912a7ee..aa50a0d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,11 +1,11 @@
projectVersion=0.0.1-SNAPSHOT
-projectGroup=io.micronaut.build-plugin-sourcegen
+projectGroup=io.micronaut.build.plugin.sourcegen
-title=Micronaut build-plugin-sourcegen
-projectDesc=TODO
+title=Micronaut Build Plugin Sourcegen
+projectDesc=Project for generating sources of build plugin tasks
projectUrl=https://micronaut.io
githubSlug=micronaut-projects/micronaut-build-plugin-sourcegen
-developers=Graeme Rocher
+developers=Andriy Dmytruk
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
org.gradle.configuration-cache=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 42d5c28..89c4fb8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,8 +19,16 @@
micronaut = "4.7.10"
micronaut-docs = "2.0.0"
micronaut-test = "4.6.2"
+micronaut-sourcegen = "1.6.1"
+
groovy = "4.0.24"
spock = "2.3-groovy-4.0"
+gradle-plugins-api = "8.11.1"
+maven-plugin-annotations = "3.9.0"
+maven-core = "3.9.4"
+maven-plugin-testing-harness = "3.3.0"
+
+sonatype-scan = "3.0.0"
# Managed versions appear in the BOM
# managed-somelib = "1.0"
@@ -29,6 +37,15 @@ spock = "2.3-groovy-4.0"
[libraries]
# Core
micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' }
+micronaut-sourcegen = { module = 'io.micronaut.sourcegen:micronaut-sourcegen-bom', version.ref = 'micronaut-sourcegen' }
+
+gradle-plugins-api = { module = 'dev.gradleplugins:gradle-api', version.ref = 'gradle-plugins-api' }
+maven-plugin-annotations = { module = 'org.apache.maven.plugin-tools:maven-plugin-annotations', version.ref = 'maven-plugin-annotations' }
+maven-plugin-api = { module = 'org.apache.maven:maven-plugin-api', version.ref = 'maven-core' }
+maven-core = { module = 'org.apache.maven:maven-core', version.ref = 'maven-core' }
+maven-plugin-testing-harness = { module = 'org.apache.maven.plugin-testing:maven-plugin-testing-harness', version.ref = 'maven-plugin-testing-harness' }
+
+sonatype-scan = { module = "org.sonatype.gradle.plugins:scan-gradle-plugin", version.ref = "sonatype-scan" }
#
# Managed dependencies appear in the BOM
diff --git a/micronaut-build-plugin-sourcegen-annotations/build.gradle.kts b/micronaut-build-plugin-sourcegen-annotations/build.gradle.kts
new file mode 100644
index 0000000..6d26691
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-annotations/build.gradle.kts
@@ -0,0 +1,3 @@
+plugins {
+ id("io.micronaut.build.internal.build-plugin-sourcegen-module")
+}
diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateGradlePlugin.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateGradlePlugin.java
new file mode 100644
index 0000000..289b92e
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateGradlePlugin.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+/**
+ * An annotation that triggers the generation of Gradle plugin and other types.
+ * Only single Gradle plugin should be generated. But a plugin may contain
+ * more than one task. Each task will have an corresponding extension method.
+ *
+ * @author Andriy Dmytruk
+ * @since 1.0.x
+ */
+@Documented
+@Retention(CLASS)
+@Target({ ElementType.TYPE })
+public @interface GenerateGradlePlugin {
+
+ /**
+ * The prefix to use for all names.
+ * For example if the prefix is {@code Test}, task will be generated as {@code TestTask}.
+ * The default is the annotated class name.
+ *
+ * @return The prefix
+ */
+ String namePrefix() default "";
+
+ /**
+ * Configure gradle tasks that will be generated.
+ *
+ * @return gradle task configurations
+ */
+ GenerateGradleTask[] tasks();
+
+ /**
+ * The types of classes to generate.
+ * By default, all are generated.
+ *
+ * @return The plugin types to generate.
+ */
+ Type[] types() default {
+ Type.GRADLE_TASK, Type.GRADLE_EXTENSION, Type.GRADLE_SPECIFICATION, Type.GRADLE_PLUGIN
+ };
+
+ /**
+ * @return The gradle task group that will be set for all tasks
+ */
+ String taskGroup() default "";
+
+ /**
+ * @return Whether to extend the micronaut plugin
+ */
+ boolean micronautPlugin() default true;
+
+ /**
+ * The coordinate of dependency to add, like
+ * {@code io.micronaut.jsonschema:micronaut-jsonschema-generator}.
+ *
+ * @return The dependency
+ */
+ String dependency() default "";
+
+ /**
+ * Enum defining the types that could be generated.
+ */
+ enum Type {
+ GRADLE_TASK,
+ GRADLE_SPECIFICATION,
+ GRADLE_EXTENSION,
+ GRADLE_PLUGIN
+ }
+
+ /**
+ * A configuration for generating a gradle task as part of the plugin.
+ */
+ @interface GenerateGradleTask {
+
+ /**
+ * @return The prefix to use for task name.
+ */
+ String namePrefix() default "";
+
+ /**
+ * @return The task configuration class name that has {@link PluginTask} annotation
+ */
+ String source();
+
+ /**
+ * @return The name to use for the generated extension
+ */
+ String extensionMethodName() default "";
+
+ /**
+ * @return Whether the task is cacheable
+ */
+ boolean cacheable() default true;
+
+ }
+
+
+}
diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateMavenMojo.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateMavenMojo.java
new file mode 100644
index 0000000..adda180
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/GenerateMavenMojo.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+/**
+ * An annotation that triggers the generation of Maven Mojo.
+ * A plugin can include multiple Mojos.
+ *
+ * @author Andriy Dmytruk
+ * @since 1.0.x
+ */
+@Documented
+@Retention(CLASS)
+@Target({ ElementType.TYPE })
+@Repeatable(GenerateMavenMojo.List.class)
+public @interface GenerateMavenMojo {
+
+ /**
+ * The prefix to use for the mojo name.
+ * For example if the prefix is {@code Test}, mojo will be generated as {@code TestMojo}.
+ * The default is the annotated class name.
+ *
+ * @return The prefix
+ */
+ String namePrefix() default "";
+
+ /**
+ * @return The task configuration class name that has {@link PluginTask} annotation
+ */
+ String source();
+
+ /**
+ * @return Whether to extend abstract micronaut mojo.
+ */
+ boolean micronautPlugin() default true;
+
+ /**
+ * The property prefix to use for parameters generated in Maven Mojo.
+ *
+ * @see PluginTaskParameter#globalProperty()
+ * @return The property prefix
+ */
+ String mavenPropertyPrefix() default "";
+
+ /**
+ * A container for repeated MavenMojo.
+ */
+ @Documented
+ @Retention(CLASS)
+ @Target({ ElementType.TYPE })
+ @interface List {
+
+ /**
+ * @return Repeated annotations
+ */
+ GenerateMavenMojo[] value();
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTask.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTask.java
new file mode 100644
index 0000000..131de29
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTask.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+/**
+ * An annotation that configures a plugin task.
+ * The annotation is used for generation of particular plugin implementations, like Maven
+ * Mojos or Gradle Tasks.
+ *
+ * @author Andriy Dmytruk
+ * @since 1.0.x
+ */
+@Documented
+@Retention(CLASS)
+@Target({ ElementType.TYPE })
+public @interface PluginTask {
+
+}
diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskExecutable.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskExecutable.java
new file mode 100644
index 0000000..f30f87f
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskExecutable.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+/**
+ * An annotation that configures the executable method to run for a plugin task.
+ * Should be inside a type annotated with {@link PluginTask}.
+ *
+ * @author Andriy Dmytruk
+ * @since 1.0.x
+ */
+@Documented
+@Retention(CLASS)
+@Target({ ElementType.METHOD })
+public @interface PluginTaskExecutable {
+
+}
diff --git a/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskParameter.java b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskParameter.java
new file mode 100644
index 0000000..3f692dd
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-annotations/src/main/java/io/micronaut/sourcegen/annotations/PluginTaskParameter.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+/**
+ * An annotation that configures a parameter for plugin task.
+ * Should be inside a type annotated with {@link PluginTask}.
+ *
+ *
The annotation is used during generation of particular plugin implementations, like Maven
+ * Mojos or Gradle Tasks.
+ *
+ *
Java primitives, strings, lists, maps, enums and simple records/POJOs are supported.
+ *
+ * @author Andriy Dmytruk
+ * @since 1.0.x
+ */
+@Documented
+@Retention(CLASS)
+@Target({ ElementType.FIELD })
+public @interface PluginTaskParameter {
+
+ /**
+ * Whether the parameter is required.
+ * By default, parameters are not required to ensure a simple plugin API.
+ * Parameters that have a default value should not be required.
+ *
+ * @return Whether it is required
+ */
+ boolean required() default false;
+
+ /**
+ * The default value.
+ * Is allowed only for Java primitives or enums.
+ *
+ * @return The default value
+ */
+ String defaultValue() default "";
+
+ /**
+ * Whether the parameter is plugin-internal.
+ * This means that the parameter won't get exposed as part of plugin API.
+ * Specific logic will be written by developer in plugin to set the value for this parameter.
+ * This is useful for parameters that depend on plugin-specific logic, like getting the
+ * build directory.
+ *
+ * @return Whether it is internal
+ */
+ boolean internal() default false;
+
+ /**
+ * The global property name.
+ * For maven Mojo it will correspond to {@code @Parameter(property='')} value.
+ * It has no current effect for Gradle.
+ *
+ * @return The property name
+ */
+ String globalProperty() default "";
+
+ /**
+ * Whether the file is a directory.
+ * Will only work for parameters of type {@link java.io.File}.
+ *
+ * @return Whether it is a directory.
+ */
+ boolean directory() default false;
+
+ /**
+ * Whether the parameter is output of the task.
+ * Most likely, the parameter is a file or directory.
+ *
+ * @return Whether it is output
+ */
+ boolean output() default false;
+
+ /**
+ * @return Path sensitivity to use for file parameters. This would reflect on how
+ * task executions are cached. If the path is considered equal, task won't be executed again.
+ * This has no effect for Maven.
+ */
+ PathSensitivity pathSensitivity() default PathSensitivity.ABSOLUTE;
+
+ /**
+ * Path sensitivity options.
+ * The specified part of the file path is used when detecting if the property has changed.
+ */
+ enum PathSensitivity {
+ /** The parameter is ignored for caching. **/
+ NONE,
+ /** Only name of the file is compared. **/
+ NAME_ONLY,
+ /** The relative path is compared. **/
+ RELATIVE,
+ /** The absolute path is compared. **/
+ ABSOLUTE
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-bom/build.gradle b/micronaut-build-plugin-sourcegen-bom/build.gradle
index c817783..c816630 100644
--- a/micronaut-build-plugin-sourcegen-bom/build.gradle
+++ b/micronaut-build-plugin-sourcegen-bom/build.gradle
@@ -2,3 +2,9 @@ plugins {
id 'io.micronaut.build.internal.build-plugin-sourcegen-base'
id "io.micronaut.build.internal.bom"
}
+
+micronautBuild {
+ binaryCompatibility {
+ enabled = false
+ }
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/build.gradle.kts b/micronaut-build-plugin-sourcegen-generator/build.gradle.kts
new file mode 100644
index 0000000..e7f92fd
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ id("io.micronaut.build.internal.build-plugin-sourcegen-module")
+}
+
+dependencies {
+ api(mnSourcegen.micronaut.sourcegen.model)
+ implementation(mnSourcegen.micronaut.sourcegen.generator)
+ api(mn.micronaut.core.processor)
+ implementation(projects.micronautBuildPluginSourcegenAnnotations)
+
+ testImplementation(mnSourcegen.micronaut.sourcegen.annotations)
+ testImplementation(mn.micronaut.inject.java.test)
+ testImplementation(mnSourcegen.micronaut.sourcegen.generator.java)
+
+ testImplementation(libs.gradle.plugins.api) {
+ exclude( "org.codehaus.groovy", "groovy")
+ exclude( "org.codehaus.groovy", "groovy-all")
+ }
+ testImplementation(libs.maven.plugin.annotations)
+ testImplementation(libs.maven.plugin.api)
+}
+
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/JavadocUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/JavadocUtils.java
new file mode 100644
index 0000000..c6d0e81
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/JavadocUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors;
+
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.javadoc.Javadoc;
+import com.github.javaparser.javadoc.JavadocBlockTag;
+import com.github.javaparser.javadoc.JavadocBlockTag.Type;
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.inject.ast.Element;
+import io.micronaut.inject.ast.MethodElement;
+import io.micronaut.inject.ast.PropertyElement;
+import io.micronaut.inject.processing.ProcessingException;
+import io.micronaut.inject.visitor.VisitorContext;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * A utility class for javadoc.
+ * Since processed task might be in a dependency, it helps with writing it javadoc info
+ * in a META-INF file and reading it from the file.
+ */
+@Internal
+public class JavadocUtils {
+
+ public static final String META_INF_FOLDER = "micronaut-plugin-gen/";
+ public static final String META_INF_EXTENSION = ".javadoc.txt";
+
+ /**
+ * Get the javadoc for a task. Task class element may be in a dependency.
+ * It will read the {@code .javadoc.txt} file written by plugin task visitor.
+ *
+ * @param context The visitor context
+ * @param element The element annotated with {@link io.micronaut.sourcegen.annotations.PluginTask}.
+ * @return The javadoc
+ */
+ public static @NonNull TypeJavadoc getTaskJavadoc(VisitorContext context, ClassElement element) {
+ String javadocMetaPath = "META-INF/" + META_INF_FOLDER + element.getName() + META_INF_EXTENSION;
+ ClassLoader classLoader = JavadocUtils.class.getClassLoader();
+
+ String javadoc = null;
+ Map elements = new LinkedHashMap<>();
+ try (InputStream inputStream = classLoader.getResourceAsStream(javadocMetaPath)) {
+ if (inputStream != null) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line = reader.readLine();
+ if (line != null) {
+ javadoc = parseJavadocInfo(line);
+ }
+ while ((line = reader.readLine()) != null) {
+ int i = line.indexOf(' ');
+ if (i > 0) {
+ elements.put(line.substring(0, i), line.substring(i + 1));
+ }
+ }
+ }
+ } else {
+ context.warn("Could not find javadoc META-INF file " + javadocMetaPath + " for type", element);
+ }
+ } catch (IOException e) {
+ throw new ProcessingException(element, "Could not read javadoc META-INF file " + javadocMetaPath + " for type" , e);
+ }
+
+ if (javadoc == null && elements.isEmpty()) {
+ return getSourceJavadoc(element);
+ }
+ if (javadoc != null && javadoc.isEmpty()) {
+ javadoc = null;
+ }
+ return new TypeJavadoc(
+ Optional.ofNullable(javadoc),
+ elements
+ );
+ }
+
+ /**
+ * Write javadoc meta info that could be parsed from a file.
+ *
+ * @param element The source element.
+ * @return The info
+ */
+ public static String writeJavadocInfo(ClassElement element) {
+ StringBuilder result = new StringBuilder();
+
+ TypeJavadoc javadoc = getSourceJavadoc(element);
+ result.append(formatJavadocInfo(javadoc.javadoc.orElse("")))
+ .append('\n');
+ for (Entry entry: javadoc.elements.entrySet()) {
+ result.append(entry.getKey())
+ .append(' ')
+ .append(formatJavadocInfo(entry.getValue()))
+ .append('\n');
+ }
+ return result.toString();
+ }
+
+ private static @NonNull TypeJavadoc getSourceJavadoc(ClassElement element) {
+ Javadoc parsed = StaticJavaParser.parseJavadoc(element.getDocumentation().orElse(""));
+ String javadoc = parsed.getDescription().toText();
+ Map elements = new LinkedHashMap<>();
+
+ for (JavadocBlockTag tag: parsed.getBlockTags()) {
+ if (tag.getType() == Type.PARAM) {
+ elements.put(tag.getName().orElse(null), tag.getContent().toText() + ".");
+ }
+ }
+ for (PropertyElement property: element.getBeanProperties()) {
+ Optional propertyDoc = property.getDocumentation();
+ Optional fieldDoc = property.getField().flatMap(Element::getDocumentation);
+ if (propertyDoc.isPresent()) {
+ elements.put(property.getName(), propertyDoc.get());
+ } else if (fieldDoc.isPresent()) {
+ elements.put(property.getName(), fieldDoc.get());
+ }
+ }
+ for (MethodElement method: element.getMethods()) {
+ Optional methodDoc = method.getDocumentation();
+ if (methodDoc.isPresent()) {
+ String key = method.getName() + Arrays.stream(method.getParameters()).map(p -> p.getType().getName())
+ .collect(Collectors.joining(",")) + "()";
+ elements.put(key, methodDoc.get());
+ }
+ }
+ if (javadoc.isEmpty()) {
+ javadoc = null;
+ }
+ return new TypeJavadoc(
+ Optional.ofNullable(javadoc),
+ elements
+ );
+ }
+
+ private static String formatJavadocInfo(String value) {
+ if (value == null) {
+ return "";
+ }
+ return value
+ .replaceAll("\n\\s+", "\n")
+ .replace("\\", "\\\\")
+ .replace("\n", "\\n");
+ }
+
+ private static String parseJavadocInfo(String line) {
+ return line.replaceAll("(? javadoc,
+ Map elements
+ ) {
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ModelUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ModelUtils.java
new file mode 100644
index 0000000..7e06927
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ModelUtils.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.Nullable;
+import io.micronaut.core.naming.NameUtils;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.inject.ast.EnumConstantElement;
+import io.micronaut.inject.ast.EnumElement;
+import io.micronaut.inject.ast.PropertyElement;
+import io.micronaut.inject.visitor.VisitorContext;
+import io.micronaut.sourcegen.generator.visitors.JavadocUtils.TypeJavadoc;
+import io.micronaut.sourcegen.model.ClassDef;
+import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder;
+import io.micronaut.sourcegen.model.ClassTypeDef;
+import io.micronaut.sourcegen.model.EnumDef;
+import io.micronaut.sourcegen.model.EnumDef.EnumDefBuilder;
+import io.micronaut.sourcegen.model.ExpressionDef;
+import io.micronaut.sourcegen.model.ExpressionDef.ComparisonOperation.OpType;
+import io.micronaut.sourcegen.model.ExpressionDef.MathBinaryOperation;
+import io.micronaut.sourcegen.model.MethodDef;
+import io.micronaut.sourcegen.model.ObjectDef;
+import io.micronaut.sourcegen.model.ParameterDef;
+import io.micronaut.sourcegen.model.PropertyDef;
+import io.micronaut.sourcegen.model.StatementDef;
+import io.micronaut.sourcegen.model.TypeDef;
+import io.micronaut.sourcegen.model.VariableDef;
+import io.micronaut.sourcegen.model.VariableDef.Local;
+
+import javax.lang.model.element.Modifier;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * A utility class for working with complex types, like enums and POJOs.
+ */
+@Internal
+public class ModelUtils {
+
+ private static final String CONVERT_METHOD_PREFIX = "convert";
+
+ /**
+ * A utility method for getting a parameter type.
+ * Complex types, like enums and POJOs get copied and re-mapped.
+ *
+ * @param context the Context
+ * @param packageName The package name to use for new created models
+ * @param element The element
+ * @param objects A mutable list that can be extended with new objects
+ * @return The type
+ */
+ public static TypeDef getType(
+ VisitorContext context, String packageName, ClassElement element, List objects
+ ) {
+ Optional exists = objects.stream()
+ .filter(v -> v.source().getName().equals(element.getName()))
+ .findFirst();
+ if (exists.isPresent()) {
+ return exists.get().type();
+ }
+ if (element.isEnum()) {
+ context.info("Copying plugin model for enum: " + element.getName());
+ return copyEnum(context, packageName, element, objects);
+ }
+ if (isPOJO(element)) {
+ context.info("Copying plugin model for POJO: " + element.getName());
+ return copyPOJO(context, packageName, element, objects);
+ }
+ Map typeArgs = element.getTypeArguments();
+ if (element.isAssignable(Collection.class) && typeArgs.containsKey("E") && isModel(typeArgs.get("E"))
+ ) {
+ return TypeDef.parameterized(ClassTypeDef.of(element.getType()), getType(
+ context, packageName, typeArgs.get("E"), objects
+ ));
+ }
+ return TypeDef.of(element);
+ }
+
+ /**
+ * Copy an existing enum to the plugin generated sources.
+ *
+ * @param context The visitor context
+ * @param packageName The package name to copy to
+ * @param element The element to copy
+ * @param objects A mutable list of objects, where enum will be added to
+ * @return The type of copied enum
+ */
+ private static ClassTypeDef copyEnum(
+ VisitorContext context, String packageName, ClassElement element, List objects
+ ) {
+ EnumDefBuilder enumDefBuilder = EnumDef.builder(packageName + "." + getSimpleName(element))
+ .addModifiers(Modifier.PUBLIC)
+ .addJavadoc(JavadocUtils.getTaskJavadoc(context, element).javadoc().orElse(element.getName() + " enum."));
+ if (element instanceof EnumElement enumElement) {
+ for (EnumConstantElement constant: enumElement.elements()) {
+ enumDefBuilder.addEnumConstant(constant.getName());
+ }
+ }
+ EnumDef enumDef = enumDefBuilder.build();
+ ClassTypeDef type = enumDef.asTypeDef();
+ objects.add(new GeneratedModel(enumDef, element, convertEnumMethod(type, element), type));
+ return type;
+ }
+
+ /**
+ * Copy an existing POJO to the plugin generated sources.
+ *
+ * @param context The visitor context
+ * @param packageName The package name to copy to
+ * @param element The element to copy
+ * @param objects A mutable list of objects, where enum will be added to
+ * @return The type of copied POJO
+ */
+ private static TypeDef copyPOJO(
+ VisitorContext context, String packageName, ClassElement element, List objects
+ ) {
+ TypeJavadoc javadoc = JavadocUtils.getTaskJavadoc(context, element);
+ ClassDefBuilder classDefBuilder = ClassDef.builder(packageName + "." + getSimpleName(element))
+ .addModifiers(Modifier.PUBLIC)
+ .addJavadoc(javadoc.javadoc().orElse(element.getName() + " class."))
+ .addSuperinterface(TypeDef.of(Serializable.class));
+ List properties = new ArrayList<>();
+ for (PropertyElement property: element.getBeanProperties()) {
+ String propertyDoc = javadoc.elements().containsKey(property.getName()) ?
+ javadoc.elements().get(property.getName()) :
+ property.getName() + " property.";
+ TypeDef type = getType(context, packageName, property.getType(), objects);
+ PropertyDef propertyDef = PropertyDef.builder(property.getName())
+ .addModifiers(Modifier.PUBLIC)
+ .ofType(type)
+ .addJavadoc(propertyDoc)
+ .build();
+ properties.add(propertyDef);
+ classDefBuilder.addProperty(propertyDef);
+ }
+ for (int i = 0; i < properties.size(); i++) {
+ classDefBuilder.addMethod(createWither(
+ properties, javadoc.elements().get(properties.get(i).getName()), i
+ ));
+ }
+ ClassDef classDef = classDefBuilder
+ .addAllFieldsConstructor(Modifier.PUBLIC)
+ .addConstructor(Collections.emptyList(), Modifier.PUBLIC)
+ .build();
+ ClassTypeDef type = classDef.asTypeDef();
+ objects.add(new GeneratedModel(classDef, element, convertPOJOMethod(type, element), type));
+ return type;
+ }
+
+ /**
+ * Converts a parameter value if required.
+ * Conversion is required if the value is a model, so a new type was generated for it
+ * instead of the original one.
+ *
+ * @param type The type
+ * @param name The name to use for local variable
+ * @param statements The modifiable statements to which a local variable may be added if needed
+ * @param paramExpression The current expression for param
+ * @return The new expression for param
+ */
+ public static ExpressionDef convertParameterIfRequired(
+ ClassElement type, String name, List statements, ExpressionDef paramExpression
+ ) {
+ if (isModel(type)) {
+ ClassTypeDef requiredType = ClassTypeDef.of(type);
+ VariableDef.Local param = new VariableDef.Local(name, requiredType);
+ String simpleName = getSimpleName(type);
+ statements.add(param.defineAndAssign(new VariableDef.This()
+ .invoke(CONVERT_METHOD_PREFIX + simpleName, requiredType, paramExpression)));
+ return param;
+ }
+ Map typeArgs = type.getTypeArguments();
+ if (type.isAssignable(Collection.class) && typeArgs.containsKey("E") && isModel(typeArgs.get("E"))) {
+ Local param = new VariableDef.Local(name, ClassTypeDef.of(type));
+ return convertCollectionParameter(param, typeArgs.get("E"), param, statements, type.isAssignable(Set.class));
+ }
+ return paramExpression;
+ }
+
+ private static ExpressionDef convertCollectionParameter(
+ Local localVar, ClassElement arg, ExpressionDef paramExpression, List statements, boolean isSet
+ ) {
+ String simpleName = getSimpleName(arg);
+ statements.add(localVar.defineAndAssign(ExpressionDef.constant(null)));
+ List innerStatements = new ArrayList<>();
+ innerStatements.add(localVar.assign(
+ isSet ? ClassTypeDef.of(HashSet.class).instantiate()
+ : ClassTypeDef.of(ArrayList.class).instantiate()
+ ));
+ Local i = new VariableDef.Local("i", TypeDef.primitive(int.class));
+ innerStatements.add(i.defineAndAssign(ExpressionDef.constant(0)));
+ innerStatements.add(new StatementDef.While(
+ i.compare(OpType.LESS_THAN, paramExpression.invoke("size", TypeDef.primitive(int.class))),
+ StatementDef.multi(
+ localVar.invoke("add", TypeDef.VOID,
+ new VariableDef.This().invoke(CONVERT_METHOD_PREFIX + simpleName, TypeDef.of(arg),
+ paramExpression.invoke("get", TypeDef.OBJECT, i)
+ )
+ ),
+ i.assign(i.math(MathBinaryOperation.OpType.ADDITION, ExpressionDef.constant(1)))
+ )
+ ));
+ statements.add(new StatementDef.If(paramExpression.compare(OpType.NOT_EQUAL_TO, ExpressionDef.constant(null)), StatementDef.multi(innerStatements)));
+ return localVar;
+ }
+
+ /**
+ * Create a wither method.
+ *
+ * @param properties The properties
+ * @param javadoc The javadoc for property
+ * @param index The property index
+ * @return The wither method
+ */
+ private static MethodDef createWither(List properties, @Nullable String javadoc, int index) {
+ PropertyDef property = properties.get(index);
+ return MethodDef.builder("with" + NameUtils.capitalize(property.getName()))
+ .addJavadoc("Create a copy and set " + property.getName() + "."
+ + (javadoc != null ? "\n" + javadoc : "")
+ )
+ .addParameter(ParameterDef.of(property.getName(), property.getType()))
+ .addModifiers(Modifier.PUBLIC)
+ .returns(TypeDef.THIS)
+ .build((t, params) -> {
+ List constructorValues = new ArrayList<>();
+ for (int j = 0; j < properties.size(); j++) {
+ if (index == j) {
+ constructorValues.add(params.get(0));
+ } else {
+ constructorValues.add(t.field(
+ properties.get(j).getName(), properties.get(j).getType()
+ ));
+ }
+ }
+ return new StatementDef.Return(TypeDef.THIS.instantiate(constructorValues));
+ });
+ }
+
+ private static MethodDef convertEnumMethod(TypeDef type, ClassElement requiredType) {
+ String simpleName = getSimpleName(requiredType);
+ ClassTypeDef outputType = ClassTypeDef.of(requiredType);
+ return MethodDef.builder(CONVERT_METHOD_PREFIX + simpleName)
+ .returns(TypeDef.of(requiredType))
+ .addParameter("value", type)
+ .build((t, params) -> new StatementDef.IfElse(
+ params.get(0).isNull(),
+ ExpressionDef.constant(null).returning(),
+ outputType.invokeStatic("valueOf", outputType,
+ params.get(0).invoke("name", TypeDef.STRING)
+ ).returning()
+ ));
+ }
+
+ private static MethodDef convertPOJOMethod(TypeDef type, ClassElement requiredType) {
+ String simpleName = getSimpleName(requiredType);
+ return MethodDef.builder(CONVERT_METHOD_PREFIX + simpleName)
+ .returns(TypeDef.of(requiredType))
+ .addParameter("value", type)
+ .build((t, params) -> {
+ List statements = new ArrayList<>();
+ Map args = new HashMap<>();
+ for (PropertyElement property: requiredType.getBeanProperties()) {
+ args.put(property.getName(), convertParameterIfRequired(
+ property.getType(),
+ NameUtils.capitalize(property.getName()) + "Param",
+ statements,
+ params.get(0).invoke("get" + NameUtils.capitalize(property.getName()), TypeDef.OBJECT)
+ ));
+ }
+ Local result = PluginUtils.instantiateType(requiredType, "result", args, statements);
+ statements.add(result.returning());
+
+ return new StatementDef.IfElse(
+ params.get(0).isNull(),
+ ExpressionDef.constant(null).returning(),
+ StatementDef.multi(statements)
+ );
+ });
+ }
+
+ private static String getSimpleName(ClassElement element) {
+ String simpleName = element.getSimpleName();
+ int index = simpleName.indexOf("$");
+ if (index >= 0) {
+ simpleName = simpleName.substring(index + 1);
+ }
+ return simpleName;
+ }
+
+ /**
+ * Whether it is considered a POJO.
+ *
+ * @param element The type
+ * @return Whether it is POJO
+ */
+ public static boolean isPOJO(ClassElement element) {
+ return !element.isEnum()
+ && !element.isPrimitive()
+ && !element.getPackageName().equals("java.util")
+ && !element.getPackageName().equals("java.lang")
+ && !element.getPackageName().equals("java.io");
+ }
+
+ /**
+ * Whether the type is a model, in which case it will be copied.
+ *
+ * @param type The type
+ * @return Whether it is a model
+ */
+ public static boolean isModel(ClassElement type) {
+ return type.isEnum() || isPOJO(type);
+ }
+
+ /**
+ * A record for holding the generated model.
+ *
+ * @param model The generated model object def
+ * @param source The source of the model
+ * @param convertorMethod The method that converts model to source
+ * @param type The type of the mode
+ */
+ public record GeneratedModel(
+ ObjectDef model,
+ ClassElement source,
+ MethodDef convertorMethod,
+ TypeDef type
+ ) {
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginTaskConfigValidatingVisitor.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginTaskConfigValidatingVisitor.java
new file mode 100644
index 0000000..3d5fbbb
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginTaskConfigValidatingVisitor.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.inject.ast.PropertyElement;
+import io.micronaut.inject.processing.ProcessingException;
+import io.micronaut.inject.visitor.TypeElementVisitor;
+import io.micronaut.inject.visitor.VisitorContext;
+import io.micronaut.sourcegen.annotations.PluginTask;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The visitor that validates a PluginTaskConfig annotated type.
+ * It also creates a META-INF file with javadoc that will be used for plugin generation.
+ *
+ * @author Andriy Dmytruk
+ * @since 1.0.x
+ */
+@Internal
+public final class PluginTaskConfigValidatingVisitor implements TypeElementVisitor {
+
+ private final Set processed = new HashSet<>();
+
+ @Override
+ public @NonNull VisitorKind getVisitorKind() {
+ return VisitorKind.ISOLATING;
+ }
+
+ @Override
+ public void start(VisitorContext visitorContext) {
+ processed.clear();
+ }
+
+ @Override
+ public Set getSupportedAnnotationNames() {
+ return Set.of(PluginTask.class.getName());
+ }
+
+ @Override
+ public void visitClass(ClassElement element, VisitorContext context) {
+ if (processed.contains(element.getName())) {
+ return;
+ }
+
+ // Verify that method is present
+ PluginUtils.getTaskExecutable(element);
+
+ writeJavaDocForType(context, element);
+ }
+
+ private void writeJavaDocForType(VisitorContext context, ClassElement element) {
+ writeJavaDocMetaInfFile(element, context);
+
+ for (PropertyElement property: element.getBeanProperties()) {
+ ClassElement propertyType = property.getType();
+ if (processed.contains(propertyType.getName())) {
+ continue;
+ }
+ if (!propertyType.isEnum() || !ModelUtils.isPOJO(propertyType)) {
+ continue;
+ }
+ processed.add(propertyType.getName());
+ writeJavaDocForType(context, propertyType);
+ }
+ }
+
+ private void writeJavaDocMetaInfFile(ClassElement element, VisitorContext context) {
+ context.info("Writing javadoc META-INF file for " + element.getName());
+ String fileName = JavadocUtils.META_INF_FOLDER + element.getName() + JavadocUtils.META_INF_EXTENSION;
+ context.visitMetaInfFile(fileName, element)
+ .ifPresent(generatedFile -> {
+ try {
+ generatedFile.write(writer ->
+ writer.write(JavadocUtils.writeJavadocInfo(element))
+ );
+ } catch (Exception e) {
+ throw new ProcessingException(element, "Failed to generate '" + fileName + "': " + e.getMessage(), e);
+ }
+ });
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginUtils.java
new file mode 100644
index 0000000..c6f02be
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/PluginUtils.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors;
+
+import io.micronaut.core.annotation.AnnotationValue;
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.annotation.Nullable;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.inject.ast.FieldElement;
+import io.micronaut.inject.ast.MethodElement;
+import io.micronaut.inject.ast.ParameterElement;
+import io.micronaut.inject.ast.PropertyElement;
+import io.micronaut.inject.processing.ProcessingException;
+import io.micronaut.sourcegen.annotations.PluginTaskExecutable;
+import io.micronaut.sourcegen.annotations.PluginTaskParameter;
+import io.micronaut.sourcegen.annotations.PluginTaskParameter.PathSensitivity;
+import io.micronaut.sourcegen.model.ClassTypeDef;
+import io.micronaut.sourcegen.model.ExpressionDef;
+import io.micronaut.sourcegen.model.StatementDef;
+import io.micronaut.sourcegen.model.TypeDef;
+import io.micronaut.sourcegen.model.VariableDef;
+import io.micronaut.sourcegen.model.VariableDef.Local;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Common utility methods for plugin generation.
+ */
+@Internal
+public class PluginUtils {
+
+ /**
+ * Validate and get the method name of the task executable.
+ *
+ * @param source The source element annotated with {@link io.micronaut.sourcegen.annotations.PluginTask}.
+ * @return The method name
+ */
+ public static MethodElement getTaskExecutable(ClassElement source) {
+ List executables = source.getMethods().stream()
+ .filter(m -> m.hasAnnotation(PluginTaskExecutable.class))
+ .toList();
+
+ if (executables.size() != 1) {
+ throw new ProcessingException(source, "Expected exactly one method annotated with @PluginTaskExecutable but found " + executables.size());
+ }
+ if (executables.get(0).getParameters().length != 0) {
+ throw new ProcessingException(source, "Expected @PluginTaskExecutable method to have no parameters");
+ }
+ if (!executables.get(0).getReturnType().isVoid()) {
+ throw new ProcessingException(source, "Expected @PluginTaskExecutable to have void return type");
+ }
+ return executables.get(0);
+ }
+
+ /**
+ * Get configuration for a plugin parameter.
+ *
+ * @param sourceJavadoc The javadoc for the task type
+ * @param property The property representing the parameter
+ * @param type The type to use for parameter
+ * @return THe configuration
+ */
+ public static @NonNull ParameterConfig getParameterConfig(
+ @NonNull JavadocUtils.TypeJavadoc sourceJavadoc, @NonNull PropertyElement property, @Nullable TypeDef type
+ ) {
+ AnnotationValue annotation = property.getAnnotation(PluginTaskParameter.class);
+ String javadoc = sourceJavadoc.elements().get(property.getName());
+ if (javadoc == null) {
+ javadoc = "Configurable " + property.getName() + " parameter.";
+ }
+ if (type == null) {
+ type = TypeDef.of(property.getType());
+ }
+ if (annotation == null) {
+ return new ParameterConfig(property, false, null, false, false, false, null, javadoc, type, PathSensitivity.ABSOLUTE);
+ }
+ return new ParameterConfig(
+ property,
+ annotation.booleanValue("required").orElse(false),
+ annotation.stringValue("defaultValue").orElse(null),
+ annotation.booleanValue("internal").orElse(false),
+ annotation.booleanValue("directory").orElse(false),
+ annotation.booleanValue("output").orElse(false),
+ annotation.stringValue("globalProperty").orElse(null),
+ javadoc,
+ type,
+ annotation.enumValue("pathSensitivity", PathSensitivity.class).orElse(PathSensitivity.ABSOLUTE)
+ );
+ }
+
+ /**
+ * Instantiate a type.
+ *
+ * @param element The class element
+ * @param name The name to use for the local variable
+ * @param arguments The arguments provided for type creation corresponding to properties
+ * @param statements A mutable statements list
+ * @return The local variable representing the instantiated type
+ */
+ public static VariableDef.Local instantiateType(
+ ClassElement element, String name, Map arguments, List statements
+ ) {
+ ClassTypeDef taskType = ClassTypeDef.of(element);
+ Local local = new Local(name, taskType);
+
+ MethodElement constructor = element.getPrimaryConstructor().orElse(null);
+ if (constructor == null) {
+ throw new ProcessingException(element, "No constructor found for " + element.getName());
+ }
+ if (element.isRecord()) {
+ List argsSorted = element.getBeanProperties().stream()
+ .map(prop -> arguments.get(prop.getName()))
+ .toList();
+ statements.add(new StatementDef.DefineAndAssign(local,
+ taskType.instantiate(constructor, argsSorted)
+ ));
+ } else {
+ Set fulfilledArgs = new HashSet<>();
+
+ constructor.getParameters();
+ List constructorArgs = new ArrayList<>();
+ for (ParameterElement param : constructor.getParameters()) {
+ fulfilledArgs.add(param.getName());
+ constructorArgs.add(arguments.containsKey(param.getName())
+ ? arguments.get(param.getName()) : ExpressionDef.constant(null));
+ }
+ statements.add(local.defineAndAssign(
+ taskType.instantiate(constructor, constructorArgs)));
+
+ for (PropertyElement property : element.getBeanProperties()) {
+ setProperty(property, fulfilledArgs, arguments, local, statements);
+ }
+ }
+ return local;
+ }
+
+ /**
+ * Set a property.
+ *
+ * @param property The property
+ * @param fulfilledArgs The values that were fulfilled already
+ * @param arguments The argument values
+ * @param owner The owner to set property to
+ * @param statements The modifiable statements
+ */
+ private static void setProperty(
+ PropertyElement property, Set fulfilledArgs,
+ Map arguments, Local owner, List statements
+ ) {
+ if (fulfilledArgs.contains(property.getName())
+ || !arguments.containsKey(property.getName())
+ ) {
+ return;
+ }
+ Optional writeMethod = property.getWriteMethod();
+ Optional field = property.getField();
+ if (writeMethod.isPresent()) {
+ statements.add(owner.invoke(writeMethod.get(), arguments.get(property.getName())));
+ fulfilledArgs.add(property.getName());
+ } else if (field.isPresent()
+ && (property.isPublic() || property.isPackagePrivate())
+ ) {
+ statements.add(owner.field(field.get()).assign(arguments.get(property.getName())));
+ fulfilledArgs.add(property.getName());
+ }
+ }
+
+ /**
+ * A common method for executing the main task executable.
+ *
+ * @param source The source annotated with {@link io.micronaut.sourcegen.annotations.PluginTask}
+ * @param methodName The name of the method annotated with {@link PluginTaskExecutable}
+ * @param arguments The prepared arguments for the task by name
+ * @return The statements to execute the task method
+ */
+ public static StatementDef executeTaskMethod(
+ ClassElement source, String methodName, Map arguments
+ ) {
+ List statements = new ArrayList<>();
+ Local task = instantiateType(source, "task", arguments, statements);
+ statements.add(task.invoke(methodName, TypeDef.VOID));
+ return StatementDef.multi(statements);
+ }
+
+ /**
+ * Configuration for a plugin parameter.
+ *
+ * @param source The source parameter
+ * @param required Whether it is required
+ * @param defaultValue The default value
+ * @param internal Whether it is internal
+ * @param directory Whether it is a directory
+ * @param output Whether it is an output
+ * @param globalProperty A global property
+ * @param javadoc The javadoc for property
+ * @param type The type to use for generated property
+ * @param pathSensitivity The path sensitivity
+ */
+ public record ParameterConfig(
+ @NonNull PropertyElement source,
+ boolean required,
+ @Nullable String defaultValue,
+ boolean internal,
+ boolean directory,
+ boolean output,
+ @Nullable String globalProperty,
+ @NonNull String javadoc,
+ @NonNull TypeDef type,
+ @NonNull PathSensitivity pathSensitivity
+ ) {
+ }
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginGenerationTriggerAnnotationVisitor.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginGenerationTriggerAnnotationVisitor.java
new file mode 100644
index 0000000..d2f6702
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginGenerationTriggerAnnotationVisitor.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.gradle;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.inject.processing.ProcessingException;
+import io.micronaut.inject.visitor.TypeElementVisitor;
+import io.micronaut.inject.visitor.VisitorContext;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type;
+import io.micronaut.sourcegen.generator.SourceGenerator;
+import io.micronaut.sourcegen.generator.SourceGenerators;
+import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradleTaskConfig;
+import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleExtensionBuilder;
+import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradlePluginBuilder;
+import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleSpecificationBuilder;
+import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTaskBuilder;
+import io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTypeBuilder;
+import io.micronaut.sourcegen.model.ObjectDef;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The visitor for generating Gradle plugins.
+ *
+ * @author Andriy Dmytruk
+ * @since 1.0.x
+ */
+@Internal
+public final class GradlePluginGenerationTriggerAnnotationVisitor implements TypeElementVisitor {
+
+ private static final List BUILDERS = List.of(
+ new GradleTaskBuilder(),
+ new GradleExtensionBuilder(),
+ new GradleSpecificationBuilder(),
+ new GradlePluginBuilder()
+ );
+
+ private final Set generated = new HashSet<>();
+ private final Set processed = new HashSet<>();
+
+ @Override
+ public @NonNull VisitorKind getVisitorKind() {
+ return VisitorKind.ISOLATING;
+ }
+
+ @Override
+ public void start(VisitorContext visitorContext) {
+ processed.clear();
+ }
+
+ @Override
+ public Set getSupportedAnnotationNames() {
+ return Set.of(GenerateGradlePlugin.class.getName());
+ }
+
+ @Override
+ public void visitClass(ClassElement element, VisitorContext context) {
+ context.info("Creating plugin classes");
+ if (processed.contains(element.getName()) || !element.hasAnnotation(GenerateGradlePlugin.class)) {
+ return;
+ }
+ try {
+ List definitions = createDefinitions(context, element);
+ SourceGenerator sourceGenerator = SourceGenerators.findByLanguage(context.getLanguage()).orElse(null);
+ if (sourceGenerator == null) {
+ throw new ProcessingException(element, "Could not find SourceGenerator for language " + context.getLanguage());
+ }
+ processed.add(element.getName());
+ for (ObjectDef definition : definitions) {
+ if (generated.contains(definition.getName())) {
+ continue;
+ }
+ generated.add(definition.getName());
+ sourceGenerator.write(definition, context, element);
+ }
+ } catch (ProcessingException e) {
+ throw e;
+ } catch (Exception e) {
+ SourceGenerators.handleFatalException(element, GenerateGradlePlugin.class, e,
+ (exception -> {
+ processed.remove(element.getName());
+ throw exception;
+ })
+ );
+ }
+ }
+
+ private List createDefinitions(VisitorContext context, ClassElement element) {
+ List definitions = new ArrayList<>();
+ GradlePluginConfig pluginConfig = GradlePluginUtils.getPluginConfig(element, context);
+ for (GradleTaskConfig taskConfig : pluginConfig.tasks()) {
+ definitions.addAll(taskConfig.generatedModels().stream().map(GeneratedModel::model).toList());
+ }
+ for (Type type : pluginConfig.types()) {
+ List typeDefinitions = null;
+ for (GradleTypeBuilder gradleTypeBuilder : BUILDERS) {
+ if (gradleTypeBuilder.getType().equals(type)) {
+ typeDefinitions = gradleTypeBuilder.build(pluginConfig);
+ }
+ }
+ if (typeDefinitions == null) {
+ throw new ProcessingException(element, "Building plugin sources of type " + type + " not supported!");
+ }
+ definitions.addAll(typeDefinitions);
+ }
+
+ return definitions;
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginUtils.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginUtils.java
new file mode 100644
index 0000000..3e7e9fc
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/GradlePluginUtils.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.gradle;
+
+import io.micronaut.core.annotation.AnnotationValue;
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.inject.ast.PropertyElement;
+import io.micronaut.inject.processing.ProcessingException;
+import io.micronaut.inject.visitor.VisitorContext;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.GenerateGradleTask;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type;
+import io.micronaut.sourcegen.generator.visitors.JavadocUtils;
+import io.micronaut.sourcegen.generator.visitors.JavadocUtils.TypeJavadoc;
+import io.micronaut.sourcegen.generator.visitors.ModelUtils;
+import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel;
+import io.micronaut.sourcegen.generator.visitors.PluginUtils;
+import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig;
+import io.micronaut.sourcegen.model.TypeDef;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility class for Gradle plugin generation.
+ */
+@Internal
+public final class GradlePluginUtils {
+
+ /**
+ * Get task configurations configured for a given element
+ * with {@link GenerateGradlePlugin} annotation.
+ *
+ * @param element The element
+ * @param context The visitor context
+ * @return The maven task config
+ */
+ public static @NonNull GradlePluginConfig getPluginConfig(
+ @NonNull ClassElement element,
+ @NonNull VisitorContext context
+ ) {
+ AnnotationValue annotation = element.getAnnotation(GenerateGradlePlugin.class);
+
+ List taskConfigs = new ArrayList<>();
+ for (AnnotationValue taskAnn:
+ annotation.getAnnotations("tasks", GenerateGradleTask.class)
+ ) {
+ taskConfigs.add(getTaskConfig(element, taskAnn, context));
+ }
+
+ return new GradlePluginConfig(
+ taskConfigs,
+ element.getPackageName(),
+ annotation.stringValue("namePrefix").orElse(element.getSimpleName()),
+ annotation.stringValue("taskGroup").orElse(null),
+ annotation.booleanValue("micronautPlugin").orElse(true),
+ annotation.stringValue("dependency").orElse(null),
+ Arrays.stream(annotation.getRequiredValue("types", Type[].class)).toList()
+ );
+ }
+
+ private static @NonNull GradleTaskConfig getTaskConfig(
+ @NonNull ClassElement element,
+ @NonNull AnnotationValue annotation,
+ @NonNull VisitorContext context
+ ) {
+ ClassElement source = annotation.stringValue("source")
+ .flatMap(context::getClassElement).orElse(null);
+ if (source == null) {
+ throw new ProcessingException(element, "Could not load source type defined in @GenerateGradleTask: "
+ + annotation.stringValue("source"));
+ }
+
+ List generatedModels = new ArrayList<>();
+ TypeJavadoc javadoc = JavadocUtils.getTaskJavadoc(context, source);
+ List parameters = new ArrayList<>();
+ for (PropertyElement property: source.getBeanProperties()) {
+ TypeDef type = ModelUtils.getType(context, element.getPackageName() + ".model",
+ property.getType(), generatedModels);
+ parameters.add(PluginUtils.getParameterConfig(javadoc, property, type));
+ }
+
+ String namePrefix = annotation.stringValue("namePrefix").orElse(source.getSimpleName());
+ String methodName = PluginUtils.getTaskExecutable(source).getName();
+ String methodJavadoc = javadoc.elements().get(methodName + "()");
+ if (methodJavadoc == null) {
+ methodJavadoc = "Main execution of " + namePrefix + " task.";
+ }
+ return new GradleTaskConfig(
+ source,
+ parameters,
+ methodName,
+ namePrefix,
+ annotation.stringValue("extensionMethodName").orElse(methodName),
+ javadoc.javadoc().orElse(namePrefix + " Gradle task."),
+ methodJavadoc,
+ generatedModels,
+ annotation.booleanValue("cacheable").orElse(true)
+ );
+ }
+
+ /**
+ * Configuration for a gradle plugin.
+ *
+ * @param tasks The task configuration
+ * @param packageName The package name
+ * @param namePrefix The type name prefix
+ * @param taskGroup The gradle group to use
+ * @param micronautPlugin Whether to extend micronaut plugin
+ * @param dependency The dependency
+ * @param types The types to generate
+ */
+ public record GradlePluginConfig(
+ List tasks,
+ String packageName,
+ String namePrefix,
+ String taskGroup,
+ boolean micronautPlugin,
+ String dependency,
+ List types
+ ) {
+ }
+
+ /**
+ * Configuration for a gradle task.
+ *
+ * @param source The source element
+ * @param parameters The parameters
+ * @param methodName The run method name
+ * @param namePrefix The prefix to use for classnames
+ * @param extensionMethodName The method name for gradle extension
+ * @param methodJavadoc The javadoc for executable method
+ * @param taskJavadoc The javadoc for the whole task
+ * @param generatedModels Additional generated models
+ * @param cacheable Whether the task should be cacheable
+ */
+ public record GradleTaskConfig (
+ @NonNull ClassElement source,
+ @NonNull List parameters,
+ @NonNull String methodName,
+ @NonNull String namePrefix,
+ @NonNull String extensionMethodName,
+ @NonNull String taskJavadoc,
+ @NonNull String methodJavadoc,
+ @NonNull List generatedModels,
+ boolean cacheable
+ ) {
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleExtensionBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleExtensionBuilder.java
new file mode 100644
index 0000000..ce24c3d
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleExtensionBuilder.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.gradle.builder;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.naming.NameUtils;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type;
+import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradleTaskConfig;
+import io.micronaut.sourcegen.model.ClassDef;
+import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder;
+import io.micronaut.sourcegen.model.ClassTypeDef;
+import io.micronaut.sourcegen.model.ExpressionDef;
+import io.micronaut.sourcegen.model.FieldDef;
+import io.micronaut.sourcegen.model.InterfaceDef;
+import io.micronaut.sourcegen.model.InterfaceDef.InterfaceDefBuilder;
+import io.micronaut.sourcegen.model.MethodDef;
+import io.micronaut.sourcegen.model.ObjectDef;
+import io.micronaut.sourcegen.model.ParameterDef;
+import io.micronaut.sourcegen.model.StatementDef;
+import io.micronaut.sourcegen.model.TypeDef;
+import io.micronaut.sourcegen.model.VariableDef;
+import io.micronaut.sourcegen.model.VariableDef.Local;
+import io.micronaut.sourcegen.model.VariableDef.MethodParameter;
+
+import javax.lang.model.element.Modifier;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTaskBuilder.TASK_SUFFIX;
+import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTaskBuilder.createGradleProperty;
+
+/**
+ * A builder for {@link Type#GRADLE_EXTENSION}.
+ * Creates a Gradle extension for calling a gradle task with the specification.
+ */
+@Internal
+public class GradleExtensionBuilder implements GradleTypeBuilder {
+
+ public static final String EXTENSION_NAME_SUFFIX = "Extension";
+ public static final String DEFAULT_EXTENSION_NAME_PREFIX = "Default";
+ public static final String TASK_CONFIGURATOR_SUFFIX = "TaskConfigurator";
+
+ private static final String PROJECT_FIELD = "project";
+ private static final String CLASSPATH_FIELD = "classpath";
+ private static final TypeDef PROJECT_TYPE = TypeDef.of("org.gradle.api.Project");
+ private static final TypeDef CONFIGURATION_TYPE = TypeDef.of("org.gradle.api.artifacts.Configuration");
+ private static final ClassTypeDef ACTION_TYPE = ClassTypeDef.of("org.gradle.api.Action");
+
+ @Override
+ public Type getType() {
+ return Type.GRADLE_EXTENSION;
+ }
+
+ @Override
+ @NonNull
+ public List build(GradlePluginConfig pluginConfig) {
+ return List.of(
+ buildExtensionInterface(pluginConfig),
+ buildDefaultExtension(pluginConfig)
+ );
+ }
+
+ private ObjectDef buildExtensionInterface(GradlePluginConfig pluginConfig) {
+ InterfaceDefBuilder builder = InterfaceDef.builder(pluginConfig.packageName() + "." + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX)
+ .addModifiers(Modifier.PUBLIC)
+ .addJavadoc("Configures the " + pluginConfig.namePrefix() + " execution.");
+
+ for (GradleTaskConfig taskConfig: pluginConfig.tasks()) {
+ ClassTypeDef specificationType = ClassTypeDef.of(pluginConfig.packageName()
+ + "." + taskConfig.namePrefix() + GradleSpecificationBuilder.SPECIFICATION_NAME_SUFFIX);
+ ClassTypeDef actionType = TypeDef.parameterized(
+ ACTION_TYPE, TypeDef.wildcardSupertypeOf(specificationType));
+
+ builder.addMethod(MethodDef.builder(taskConfig.extensionMethodName())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addParameter("name", String.class)
+ .addParameter(ParameterDef.builder("action", actionType).build())
+ .addJavadoc("Create a task for " + taskConfig.extensionMethodName() + "." +
+ "\n" + taskConfig.methodJavadoc() +
+ "\n@param name The unique identifier used to derive task names" +
+ "\n@param spec The configurable specification"
+ )
+ .build()
+ );
+ }
+ return builder.build();
+ }
+
+ private ObjectDef buildDefaultExtension(GradlePluginConfig pluginConfig) {
+ TypeDef interfaceType = TypeDef.of(pluginConfig.packageName() + "." + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX);
+
+ ClassDefBuilder builder = ClassDef.builder(pluginConfig.packageName() +
+ "." + DEFAULT_EXTENSION_NAME_PREFIX + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX)
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addSuperinterface(interfaceType)
+ .addField(
+ FieldDef.builder("names", TypeDef.parameterized(Set.class, String.class))
+ .addModifiers(Modifier.PROTECTED, Modifier.FINAL)
+ .initializer(ClassTypeDef.of(HashSet.class).instantiate())
+ .build()
+ )
+ .addField(FieldDef.builder(PROJECT_FIELD, PROJECT_TYPE)
+ .addModifiers(Modifier.PROTECTED, Modifier.FINAL).build())
+ .addField(FieldDef.builder(CLASSPATH_FIELD, CONFIGURATION_TYPE)
+ .addModifiers(Modifier.PROTECTED, Modifier.FINAL).build());
+
+ builder.addMethod(MethodDef.builder(MethodDef.CONSTRUCTOR)
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation("javax.inject.Inject")
+ .addParameter(PROJECT_FIELD, PROJECT_TYPE)
+ .addParameter(CLASSPATH_FIELD, CONFIGURATION_TYPE)
+ .build((t, params) ->
+ StatementDef.multi(
+ t.field(PROJECT_FIELD, PROJECT_TYPE).assign(params.get(0)),
+ t.field(CLASSPATH_FIELD, CONFIGURATION_TYPE).assign(params.get(1))
+ )
+ ));
+
+ for (GradleTaskConfig taskConfig: pluginConfig.tasks()) {
+ ClassTypeDef specificationType = ClassTypeDef.of(pluginConfig.packageName()
+ + "." + taskConfig.namePrefix() + GradleSpecificationBuilder.SPECIFICATION_NAME_SUFFIX);
+ ClassTypeDef actionType = TypeDef.parameterized(
+ ACTION_TYPE, TypeDef.wildcardSupertypeOf(specificationType));
+
+ builder.addMethod(MethodDef.builder(taskConfig.extensionMethodName())
+ .overrides()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter("name", String.class)
+ .addParameter(ParameterDef.builder("action", actionType).build())
+ .build((t, params) -> buildExtensionMethod(t, params, pluginConfig, taskConfig, specificationType))
+ );
+ builder.addMethod(buildCreateTaskMethod(pluginConfig, taskConfig));
+ builder.addMethod(MethodDef.builder("configureSpec")
+ .addModifiers(Modifier.PROTECTED)
+ .addParameter("spec", specificationType)
+ .build((t, params) -> buildConfigureSpecMethod(taskConfig, params))
+ );
+
+ builder.addInnerType(buildTaskConfigurator(pluginConfig, taskConfig, specificationType));
+ }
+
+ return builder.build();
+ }
+
+ private ClassDef buildTaskConfigurator(
+ GradlePluginConfig pluginConfig, GradleTaskConfig taskConfig, TypeDef specificationType
+ ) {
+ ClassTypeDef taskType = ClassTypeDef.of(pluginConfig.packageName() + "." + taskConfig.namePrefix() + TASK_SUFFIX);
+ FieldDef specField = FieldDef.builder("spec", specificationType).build();
+ FieldDef classpathField = FieldDef.builder(CLASSPATH_FIELD, CONFIGURATION_TYPE).build();
+
+ MethodDef execute = MethodDef.builder("execute")
+ .addParameter(taskType)
+ .overrides()
+ .addModifiers(Modifier.PUBLIC)
+ .build((t, params) -> {
+ List statements = new ArrayList<>();
+ MethodParameter task = params.get(0);
+ if (pluginConfig.taskGroup() != null) {
+ statements.add(task.invoke("setGroup", TypeDef.VOID, ExpressionDef.constant(pluginConfig.taskGroup())));
+ }
+ statements.add(task.invoke("getClasspath", ClassTypeDef.of("org.gradle.api.file.ConfigurableFileLocation"))
+ .invoke("from", TypeDef.VOID, t.field(classpathField))
+ );
+ statements.add(task.invoke("setDescription", TypeDef.VOID,
+ ExpressionDef.constant("Configure the " + taskConfig.extensionMethodName())));
+ for (ParameterConfig parameter: taskConfig.parameters()) {
+ String getterName = "get" + NameUtils.capitalize(parameter.source().getName());
+ TypeDef getterType = createGradleProperty(parameter);
+ if (!parameter.internal()) {
+ StatementDef convention = task
+ .invoke(getterName, getterType)
+ .invoke("convention", getterType, t.field(specField).invoke(getterName, getterType));
+ statements.add(convention);
+ }
+ }
+ return StatementDef.multi(statements);
+ });
+ return ClassDef.builder(taskConfig.namePrefix() + TASK_CONFIGURATOR_SUFFIX)
+ .addModifiers(Modifier.STATIC, Modifier.PROTECTED)
+ .addSuperinterface(TypeDef.parameterized(ACTION_TYPE, taskType))
+ .addField(specField)
+ .addField(classpathField)
+ .addAllFieldsConstructor()
+ .addMethod(execute)
+ .build();
+ }
+
+ private MethodDef buildCreateTaskMethod(GradlePluginConfig pluginConfig, GradleTaskConfig taskConfig) {
+ ClassTypeDef taskType = ClassTypeDef.of(pluginConfig.packageName() + "." + taskConfig.namePrefix() + TASK_SUFFIX);
+ TypeDef taskProviderType = TypeDef.parameterized(ClassTypeDef.of("org.gradle.api.tasks.TaskProvider"), TypeDef.wildcardSubtypeOf(taskType));
+ TypeDef taskContainerType = TypeDef.of("org.gradle.api.tasks.TaskContainer");
+ TypeDef pluginConfiguratorType = TypeDef.parameterized(ACTION_TYPE, taskType);
+
+ return MethodDef.builder("create" + taskConfig.namePrefix() + "Task")
+ .returns(taskProviderType)
+ .addParameter("name", String.class)
+ .addParameter("configurator", pluginConfiguratorType)
+ .build((t, params) ->
+ t.field(PROJECT_FIELD, PROJECT_TYPE)
+ .invoke("getTasks", taskContainerType)
+ .invoke("register", taskProviderType,
+ params.get(0),
+ taskType.getStaticField("class", TypeDef.CLASS),
+ params.get(1)
+ ).returning()
+ );
+ }
+
+ private StatementDef buildConfigureSpecMethod(GradleTaskConfig taskConfig, List params) {
+ List statements = new ArrayList<>();
+ for (ParameterConfig parameter: taskConfig.parameters()) {
+ String getterName = "get" + NameUtils.capitalize(parameter.source().getName());
+ TypeDef getterType = createGradleProperty(parameter);
+ if (parameter.defaultValue() != null && !parameter.internal()) {
+ TypeDef type = parameter.type();
+ StatementDef convention = params.get(0)
+ .invoke(getterName, getterType)
+ .invoke("convention", getterType, GradleTaskBuilder.createDefault(type, parameter.defaultValue()));
+ statements.add(convention);
+ }
+ }
+ return StatementDef.multi(statements);
+ }
+
+ private StatementDef buildExtensionMethod(
+ VariableDef t, List params, GradlePluginConfig pluginConfig, GradleTaskConfig taskConfig, ClassTypeDef specificationType
+ ) {
+ StatementDef ifStatement = new StatementDef.If(
+ t.field("names", TypeDef.of(String.class)).invoke("add", TypeDef.of(boolean.class), params.get(0)).isFalse(),
+ new StatementDef.Throw(ClassTypeDef.of("org.gradle.api.GradleException")
+ .instantiate(TypeDef.STRING.invokeStatic("format", TypeDef.STRING,
+ ExpressionDef.constant("An " + taskConfig.extensionMethodName() + " definition with name '%s' was already created"),
+ params.get(0))
+ )
+ )
+ );
+ TypeDef objectFactoryType = TypeDef.of("org.gradle.api.type.ObjectFactory");
+ Local spec = new Local("spec", specificationType);
+ StatementDef specCreation = new StatementDef.DefineAndAssign(
+ spec,
+ t.field(PROJECT_FIELD, PROJECT_TYPE)
+ .invoke("getObjects", objectFactoryType)
+ .invoke("newInstance", specificationType, specificationType.getStaticField("class", TypeDef.CLASS))
+ );
+ StatementDef configureSpec = t.invoke("configureSpec", TypeDef.VOID, spec);
+ StatementDef actionCall = params.get(1).invoke("execute", TypeDef.VOID, spec);
+
+ ClassTypeDef taskType = ClassTypeDef.of(pluginConfig.packageName() + "." + taskConfig.namePrefix() + TASK_SUFFIX);
+ TypeDef taskProviderType = TypeDef.parameterized(ClassTypeDef.of("org.gradle.api.tasks.TaskProvider"), TypeDef.wildcardSubtypeOf(taskType));
+ ExpressionDef pluginConfigurator = ClassTypeDef.of(taskConfig.namePrefix() + TASK_CONFIGURATOR_SUFFIX)
+ .instantiate(spec, t.field(CLASSPATH_FIELD, CONFIGURATION_TYPE));
+ Local task = new Local("task", taskProviderType);
+ StatementDef taskCreation = new StatementDef.DefineAndAssign(
+ task,
+ t.invoke("create" + taskConfig.namePrefix() + "Task", taskProviderType, params.get(0), pluginConfigurator)
+ );
+ // TODO source sets
+ return StatementDef.multi(
+ ifStatement,
+ specCreation,
+ configureSpec,
+ actionCall,
+ taskCreation
+ );
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradlePluginBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradlePluginBuilder.java
new file mode 100644
index 0000000..fe3c9f6
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradlePluginBuilder.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.gradle.builder;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig;
+import io.micronaut.sourcegen.model.ClassDef;
+import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder;
+import io.micronaut.sourcegen.model.ClassTypeDef;
+import io.micronaut.sourcegen.model.ExpressionDef;
+import io.micronaut.sourcegen.model.FieldDef;
+import io.micronaut.sourcegen.model.MethodDef;
+import io.micronaut.sourcegen.model.ObjectDef;
+import io.micronaut.sourcegen.model.StatementDef;
+import io.micronaut.sourcegen.model.StatementDef.DefineAndAssign;
+import io.micronaut.sourcegen.model.TypeDef;
+import io.micronaut.sourcegen.model.VariableDef.Local;
+
+import javax.lang.model.element.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleExtensionBuilder.DEFAULT_EXTENSION_NAME_PREFIX;
+import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleExtensionBuilder.EXTENSION_NAME_SUFFIX;
+
+/**
+ * A builder for {@link Type#GRADLE_PLUGIN}.
+ * Creates a plugin that configures an extension and task.
+ */
+@Internal
+public class GradlePluginBuilder implements GradleTypeBuilder {
+
+ public static final String PLUGIN_SUFFIX = "Plugin";
+
+ private static final String MICRONAUT_BASE_PLUGIN = "io.micronaut.gradle.MicronautBasePlugin";
+ private static final String MICRONAUT_PLUGINS_HELPER = "io.micronaut.gradle.PluginsHelper";
+ private static final String CREATE_METHOD = "create";
+ private static final ClassTypeDef PROJECT_TYPE = ClassTypeDef.of("org.gradle.api.Project");
+ private static final ClassTypeDef CONFIGURATION_TYPE = ClassTypeDef.of("org.gradle.api.artifacts.Configuration");
+ private static final FieldDef CLASS_STATIC_FIELD = FieldDef.builder("class", TypeDef.CLASS).build();
+
+ @Override
+ public Type getType() {
+ return Type.GRADLE_PLUGIN;
+ }
+
+ @Override
+ @NonNull
+ public List build(GradlePluginConfig pluginConfig) {
+ String pluginType = pluginConfig.packageName() + "." + pluginConfig.namePrefix() + PLUGIN_SUFFIX;
+ ClassDefBuilder builder = ClassDef.builder(pluginType)
+ .addModifiers(Modifier.PUBLIC)
+ .addSuperinterface(TypeDef.parameterized(
+ ClassTypeDef.of("org.gradle.api.Plugin"),
+ PROJECT_TYPE
+ ));
+ builder.addMethod(createExtensionMethod(pluginConfig));
+ builder.addMethod(createApplyMethod(pluginConfig));
+ return List.of(builder.build());
+ }
+
+ private MethodDef createApplyMethod(GradlePluginConfig pluginConfig) {
+ ClassTypeDef extensionType = ClassTypeDef.of(pluginConfig.packageName() + "."
+ + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX);
+
+ return MethodDef.builder("apply")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter("project", ClassTypeDef.of("org.gradle.api.Project"))
+ .build((t, params) -> {
+ List statements = new ArrayList<>();
+ if (pluginConfig.micronautPlugin()) {
+ params.get(0)
+ .invoke("getPluginManager", ClassTypeDef.of("org.gradle.api.plugins.PluginManager"))
+ .invoke("apply", TypeDef.VOID, ClassTypeDef.of(MICRONAUT_BASE_PLUGIN).getStaticField(CLASS_STATIC_FIELD));
+ }
+ ExpressionDef configurations = params.get(0).invoke("getConfigurations", TypeDef.of("org.gradle.api.artifacts.ConfigurationContainer"));
+ ExpressionDef dependencyHandler = params.get(0).invoke("getDependencies", TypeDef.of("org.gradle.api.artifacts.dsl.DependencyHandler"));
+ TypeDef dependencyType = TypeDef.of("org.gradle.api.artifacts.Dependency");
+
+ Local dependencies = new Local("dependencies", CONFIGURATION_TYPE);
+ statements.add(new DefineAndAssign(
+ dependencies,
+ configurations.invoke(CREATE_METHOD, CONFIGURATION_TYPE, ExpressionDef.constant(pluginConfig.namePrefix() + "Configuration"))
+ ));
+ statements.add(dependencies.invoke("setCanBeResolved", TypeDef.VOID, ExpressionDef.constant(false)));
+ statements.add(dependencies.invoke("setCanBeConsumed", TypeDef.VOID, ExpressionDef.constant(false)));
+ statements.add(dependencies.invoke("setDescription", TypeDef.VOID, ExpressionDef.constant("The " + pluginConfig.namePrefix() + " worker dependencies")));
+ if (pluginConfig.dependency() != null) {
+ statements.add(dependencies.invoke("getDependencies", TypeDef.of("org.gradle.api.artifacts.DependencySet"))
+ .invoke("add", TypeDef.VOID, dependencyHandler.invoke(CREATE_METHOD, dependencyType, ExpressionDef.constant(pluginConfig.dependency()))));
+ }
+
+ Local classpath = new Local("classpath", CONFIGURATION_TYPE);
+ statements.add(new DefineAndAssign(
+ classpath,
+ configurations.invoke(CREATE_METHOD, CONFIGURATION_TYPE, ExpressionDef.constant(pluginConfig.namePrefix() + "Classpath"))
+ ));
+ statements.add(classpath.invoke("setCanBeResolved", TypeDef.VOID, ExpressionDef.constant(true)));
+ statements.add(classpath.invoke("setCanBeConsumed", TypeDef.VOID, ExpressionDef.constant(false)));
+ statements.add(classpath.invoke("setDescription", TypeDef.VOID, ExpressionDef.constant("The " + pluginConfig.namePrefix() + " worker classpath")));
+ statements.add(classpath.invoke("extendsFrom", TypeDef.VOID, dependencies));
+
+ statements.add(t.invoke("createExtension", extensionType, params.get(0), classpath));
+ return StatementDef.multi(statements);
+ });
+ }
+
+ private MethodDef createExtensionMethod(GradlePluginConfig pluginConfig) {
+ ClassTypeDef extensionType = ClassTypeDef.of(pluginConfig.packageName() + "." + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX);
+ ClassTypeDef defaultExtensionType = ClassTypeDef.of(pluginConfig.packageName() + "." + DEFAULT_EXTENSION_NAME_PREFIX + pluginConfig.namePrefix() + EXTENSION_NAME_SUFFIX);
+
+ return MethodDef.builder("createExtension")
+ .addModifiers(Modifier.PROTECTED)
+ .returns(extensionType)
+ .addParameter("project", PROJECT_TYPE)
+ .addParameter("classpath", CONFIGURATION_TYPE)
+ .build((t, params) -> {
+ ExpressionDef root = params.get(0);
+ if (pluginConfig.micronautPlugin()) {
+ root = ClassTypeDef.of(MICRONAUT_PLUGINS_HELPER)
+ .invokeStatic("findMicronautExtension", TypeDef.of("io.micronaut.gradle.MicronautExtension"), params.get(0));
+ }
+ ExpressionDef extensions = root.invoke("getExtensions", ClassTypeDef.of("org.gradle.api.plugins.ExtensionContainer"));
+ return new StatementDef.Return(extensions.invoke(CREATE_METHOD, extensionType,
+ extensionType.getStaticField(CLASS_STATIC_FIELD),
+ ExpressionDef.constant(pluginConfig.namePrefix()),
+ defaultExtensionType.getStaticField(CLASS_STATIC_FIELD),
+ params.get(0),
+ params.get(1)
+ ));
+ });
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleSpecificationBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleSpecificationBuilder.java
new file mode 100644
index 0000000..35a5a6e
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleSpecificationBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.gradle.builder;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.naming.NameUtils;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type;
+import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradleTaskConfig;
+import io.micronaut.sourcegen.model.InterfaceDef;
+import io.micronaut.sourcegen.model.InterfaceDef.InterfaceDefBuilder;
+import io.micronaut.sourcegen.model.MethodDef;
+import io.micronaut.sourcegen.model.MethodDef.MethodDefBuilder;
+import io.micronaut.sourcegen.model.ObjectDef;
+
+import javax.lang.model.element.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+import static io.micronaut.sourcegen.generator.visitors.gradle.builder.GradleTaskBuilder.createGradleProperty;
+
+/**
+ * A builder for {@link Type#GRADLE_SPECIFICATION}.
+ * Creates a Gradle specification for configuring a gradle task.
+ */
+@Internal
+public class GradleSpecificationBuilder implements GradleTypeBuilder {
+
+ public static final String SPECIFICATION_NAME_SUFFIX = "Spec";
+
+ @Override
+ public Type getType() {
+ return Type.GRADLE_SPECIFICATION;
+ }
+
+ @Override
+ @NonNull
+ public List build(GradlePluginConfig pluginConfig) {
+ List objects = new ArrayList<>();
+ for (GradleTaskConfig taskConfig: pluginConfig.tasks()) {
+ objects.add(buildForTask(pluginConfig.packageName(), taskConfig));
+ }
+ return objects;
+ }
+
+ private ObjectDef buildForTask(String packageName, GradleTaskConfig taskConfig) {
+ InterfaceDefBuilder builder = InterfaceDef.builder(packageName + "." + taskConfig.namePrefix() + SPECIFICATION_NAME_SUFFIX)
+ .addModifiers(Modifier.PUBLIC)
+ .addJavadoc("Specification that is used for configuring " + taskConfig.namePrefix() + " task.\n" +
+ taskConfig.taskJavadoc());
+ for (ParameterConfig parameter: taskConfig.parameters()) {
+ if (parameter.internal()) {
+ continue;
+ }
+ MethodDefBuilder propBuilder = MethodDef
+ .builder("get" + NameUtils.capitalize(parameter.source().getName()))
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addJavadoc(parameter.javadoc())
+ .returns(createGradleProperty(parameter));
+ builder.addMethod(propBuilder.build());
+ }
+ return builder.build();
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTaskBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTaskBuilder.java
new file mode 100644
index 0000000..ede0bea
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTaskBuilder.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.gradle.builder;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.convert.ConversionService;
+import io.micronaut.core.naming.NameUtils;
+import io.micronaut.core.reflect.ClassUtils;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin.Type;
+import io.micronaut.sourcegen.generator.visitors.ModelUtils;
+import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel;
+import io.micronaut.sourcegen.generator.visitors.PluginUtils;
+import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradlePluginConfig;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils.GradleTaskConfig;
+import io.micronaut.sourcegen.model.AnnotationDef;
+import io.micronaut.sourcegen.model.ClassDef;
+import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder;
+import io.micronaut.sourcegen.model.ClassTypeDef;
+import io.micronaut.sourcegen.model.ClassTypeDef.ClassDefType;
+import io.micronaut.sourcegen.model.ClassTypeDef.ClassElementType;
+import io.micronaut.sourcegen.model.EnumDef;
+import io.micronaut.sourcegen.model.ExpressionDef;
+import io.micronaut.sourcegen.model.ExpressionDef.Constant;
+import io.micronaut.sourcegen.model.FieldDef;
+import io.micronaut.sourcegen.model.InterfaceDef;
+import io.micronaut.sourcegen.model.InterfaceDef.InterfaceDefBuilder;
+import io.micronaut.sourcegen.model.MethodDef;
+import io.micronaut.sourcegen.model.MethodDef.MethodDefBuilder;
+import io.micronaut.sourcegen.model.ObjectDef;
+import io.micronaut.sourcegen.model.ParameterDef;
+import io.micronaut.sourcegen.model.StatementDef;
+import io.micronaut.sourcegen.model.TypeDef;
+import io.micronaut.sourcegen.model.VariableDef;
+
+import javax.lang.model.element.Modifier;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A builder for {@link GenerateGradlePlugin.Type#GRADLE_TASK}.
+ * Creates a task, work action and work action parameters given a plugin task configuration.
+ */
+@Internal
+public class GradleTaskBuilder implements GradleTypeBuilder {
+
+ public static final String TASK_SUFFIX = "Task";
+ public static final String WORK_ACTION_SUFFIX = "WorkAction";
+ public static final String WORK_ACTION_PARAMETERS_SUFFIX = "WorkActionParameters";
+
+ private static final String GET_CLASSPATH_METHOD = "getClasspath";
+ private static final String EXECUTE_METHOD = "execute";
+
+ @Override
+ public Type getType() {
+ return Type.GRADLE_TASK;
+ }
+
+ @Override
+ @NonNull
+ public List build(GradlePluginConfig pluginConfig) {
+ List objects = new ArrayList<>();
+ for (GradleTaskConfig taskConfig: pluginConfig.tasks()) {
+ objects.addAll(buildTask(pluginConfig.packageName(), taskConfig));
+ }
+ return objects;
+ }
+
+ private List buildTask(String packageName, GradleTaskConfig taskConfig) {
+ String taskType = packageName + "." + taskConfig.namePrefix() + TASK_SUFFIX;
+ ClassDefBuilder builder = ClassDef.builder(taskType)
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .superclass(ClassTypeDef.of("org.gradle.api.DefaultTask"))
+ .addJavadoc(taskConfig.taskJavadoc());
+ if (taskConfig.cacheable()) {
+ builder.addAnnotation("org.gradle.api.tasks.CacheableTask");
+ }
+ builder.addInnerType(createWorkAction(taskConfig));
+ builder.addInnerType(createWorkActionParameters(taskConfig));
+ builder.addInnerType(createWorkActionParameterConfigurator(TypeDef.of(taskType), taskConfig));
+ builder.addInnerType(createClasspathConfigurator(TypeDef.of(taskType), taskConfig));
+
+ for (ParameterConfig parameter: taskConfig.parameters()) {
+ builder.addMethod(createParameterGetter(parameter));
+ }
+
+ TypeDef classpathType = TypeDef.of("org.gradle.api.file.ConfigurableFileCollection");
+ builder.addMethod(MethodDef.builder(GET_CLASSPATH_METHOD)
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(classpathType)
+ .addAnnotation("org.gradle.api.tasks.Classpath")
+ .build()
+ );
+
+ TypeDef workerExecutorType = TypeDef.of("org.gradle.workers.WorkerExecutor");
+ builder.addMethod(MethodDef.builder("getWorkerExecutor")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(workerExecutorType)
+ .addAnnotation("javax.inject.Inject")
+ .build()
+ );
+
+ builder.addMethod(createExecuteMethod(taskConfig, workerExecutorType));
+ return List.of(builder.build());
+ }
+
+ private MethodDef createParameterGetter(ParameterConfig parameter) {
+ MethodDefBuilder propBuilder = MethodDef
+ .builder("get" + NameUtils.capitalize(parameter.source().getName()))
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addJavadoc(parameter.javadoc())
+ .returns(createGradleProperty(parameter));
+ if (parameter.output()) {
+ if (parameter.source().getType().isAssignable(File.class)) {
+ if (parameter.directory()) {
+ propBuilder.addAnnotation(AnnotationDef.builder(ClassTypeDef.of("org.gradle.api.tasks.OutputDirectory")).build());
+ } else {
+ propBuilder.addAnnotation(AnnotationDef.builder(ClassTypeDef.of("org.gradle.api.tasks.OutputFile")).build());
+ }
+ }
+ } else {
+ propBuilder.addAnnotation("org.gradle.api.tasks.Input");
+ if (parameter.source().getType().isAssignable(File.class)) {
+ if (parameter.directory()) {
+ propBuilder.addAnnotation(AnnotationDef.builder(ClassTypeDef.of("org.gradle.api.tasks.InputDirectory")).build());
+ } else {
+ propBuilder.addAnnotation(AnnotationDef.builder(ClassTypeDef.of("org.gradle.api.tasks.InputFile")).build());
+ }
+ ClassTypeDef pathSensitivityType = ClassTypeDef.of("org.gradle.api.tasks.PathSensitivity");
+ ClassTypeDef pathSensitiveType = ClassTypeDef.of("org.gradle.api.tasks.PathSensitive");
+ propBuilder.addAnnotation(AnnotationDef.builder(pathSensitiveType)
+ .addMember("value", pathSensitivityType.getStaticField(parameter.pathSensitivity().name(), pathSensitivityType))
+ .build()
+ );
+ }
+ }
+
+ if (!parameter.required()) {
+ propBuilder.addAnnotation("org.gradle.api.tasks.Optional");
+ }
+ return propBuilder.build();
+ }
+
+ private ClassDef createWorkActionParameterConfigurator(TypeDef taskType, GradleTaskConfig taskConfig) {
+ TypeDef parametersType = TypeDef.of(taskConfig.namePrefix() + WORK_ACTION_PARAMETERS_SUFFIX);
+ FieldDef taskField = FieldDef.builder("task").ofType(taskType).build();
+ return ClassDef.builder(taskConfig.namePrefix() + "WorkActionParameterConfigurator")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addSuperinterface(TypeDef.parameterized(
+ ClassTypeDef.of("org.gradle.api.Action"),
+ parametersType
+ ))
+ .addField(taskField)
+ .addAllFieldsConstructor(Modifier.PUBLIC)
+ .addMethod(MethodDef.builder(EXECUTE_METHOD)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(TypeDef.VOID)
+ .overrides()
+ .addParameter(ParameterDef.of("params", parametersType))
+ .build((t, params) -> {
+ List statements = new ArrayList<>();
+ for (ParameterConfig parameter: taskConfig.parameters()) {
+ String getterName = "get" + NameUtils.capitalize(parameter.source().getName());
+ TypeDef getterType = createGradleProperty(parameter);
+ ExpressionDef def = t.field(taskField).invoke(getterName, getterType);
+ if (!parameter.required()) {
+ if (parameter.defaultValue() != null) {
+ TypeDef type = parameter.type();
+ def = def.invoke(
+ "orElse",
+ type,
+ createDefault(type, parameter.defaultValue())
+ );
+ } else {
+ def = def.invoke("getOrNull", parameter.type());
+ }
+ }
+ statements.add(params.get(0)
+ .invoke(getterName, getterType)
+ .invoke("set", TypeDef.VOID, def)
+ );
+ }
+ return StatementDef.multi(statements);
+ })
+ )
+ .build();
+ }
+
+ static ExpressionDef createDefault(TypeDef type, String value) {
+ if (type instanceof ClassElementType classElementType) {
+ return ExpressionDef.constant(classElementType.classElement(), type, value);
+ } else if (type instanceof TypeDef.Primitive primitiveType) {
+ return ClassUtils.getPrimitiveType(primitiveType.name()).flatMap(t ->
+ ConversionService.SHARED.convert(value, t)
+ ).map(o -> new Constant(type, o)).orElse(null);
+ } else if (type instanceof ClassDefType classDefType && classDefType.objectDef() instanceof EnumDef) {
+ return classDefType.getStaticField(value, type);
+ }
+ throw new UnsupportedOperationException("Cannot create default value of type " + type);
+ }
+
+ private ClassDef createClasspathConfigurator(TypeDef taskType, GradleTaskConfig taskConfig) {
+ FieldDef taskField = FieldDef.builder("task").ofType(taskType).build();
+ TypeDef specType = TypeDef.of("org.gradle.workers.ClassLoaderWorkerSpec");
+ TypeDef classpathType = TypeDef.of("org.gradle.api.file.ConfigurableFileCollection");
+ return ClassDef.builder(taskConfig.namePrefix() + "ClasspathConfigurator")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addSuperinterface(TypeDef.parameterized(
+ ClassTypeDef.of("org.gradle.api.Action"),
+ specType
+ ))
+ .addField(taskField)
+ .addAllFieldsConstructor(Modifier.PUBLIC)
+ .addMethod(MethodDef.builder(EXECUTE_METHOD)
+ .addModifiers(Modifier.PUBLIC)
+ .returns(TypeDef.VOID)
+ .overrides()
+ .addParameter(ParameterDef.of("spec", specType))
+ .build((t, params) ->
+ params.get(0)
+ .invoke(GET_CLASSPATH_METHOD, classpathType)
+ .invoke("from", TypeDef.VOID, t.field(taskField).invoke(GET_CLASSPATH_METHOD, classpathType))
+ )
+ )
+ .build();
+ }
+
+ private InterfaceDef createWorkActionParameters(GradleTaskConfig taskConfig) {
+ InterfaceDefBuilder builder = InterfaceDef.builder(taskConfig.namePrefix() + WORK_ACTION_PARAMETERS_SUFFIX)
+ .addModifiers(Modifier.PUBLIC)
+ .addSuperinterface(ClassTypeDef.of("org.gradle.workers.WorkParameters"));
+ for (ParameterConfig parameter: taskConfig.parameters()) {
+ MethodDefBuilder propBuilder = MethodDef
+ .builder("get" + NameUtils.capitalize(parameter.source().getName()))
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(createGradleProperty(parameter));
+ builder.addMethod(propBuilder.build());
+ }
+ return builder.build();
+ }
+
+ private ClassDef createWorkAction(GradleTaskConfig taskConfig) {
+ ClassTypeDef parametersType = ClassTypeDef.of(taskConfig.namePrefix() + WORK_ACTION_PARAMETERS_SUFFIX);
+ MethodDef executeMethod = MethodDef
+ .builder(EXECUTE_METHOD)
+ .returns(TypeDef.VOID)
+ .addModifiers(Modifier.PUBLIC)
+ .overrides()
+ .build((t, params) -> runTask(taskConfig, t, parametersType));
+ return ClassDef.builder(taskConfig.namePrefix() + WORK_ACTION_SUFFIX)
+ .addSuperinterface(TypeDef.parameterized(
+ ClassTypeDef.of("org.gradle.workers.WorkAction"),
+ parametersType
+ ))
+ .addMethods(taskConfig.generatedModels().stream().map(GeneratedModel::convertorMethod).toList())
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT, Modifier.STATIC)
+ .addMethod(executeMethod)
+ .build();
+ }
+
+ private MethodDef createExecuteMethod(GradleTaskConfig taskConfig, TypeDef workerExecutorType) {
+ return MethodDef.builder(EXECUTE_METHOD)
+ .returns(TypeDef.VOID)
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation("org.gradle.api.tasks.TaskAction")
+ .addJavadoc(taskConfig.methodJavadoc())
+ .build((t, params) ->
+ t.invoke("getWorkerExecutor", workerExecutorType)
+ .invoke("classLoaderIsolation",
+ workerExecutorType,
+ ClassTypeDef.of(taskConfig.namePrefix() + "ClasspathConfigurator").instantiate(t)
+ )
+ .invoke("submit", TypeDef.VOID,
+ ClassTypeDef.of(taskConfig.namePrefix() + WORK_ACTION_SUFFIX).getStaticField("class", TypeDef.CLASS),
+ ClassTypeDef.of(taskConfig.namePrefix() + "WorkActionParameterConfigurator").instantiate(t)
+ )
+ );
+ }
+
+ private StatementDef runTask(GradleTaskConfig taskConfig, VariableDef.This t, ClassTypeDef parametersType) {
+ List statements = new ArrayList<>();
+ Map params = new HashMap<>();
+ statements.add(t.invoke("getParameters", parametersType).newLocal("parameters"));
+
+ for (ParameterConfig parameter: taskConfig.parameters()) {
+ ExpressionDef expression = new VariableDef.Local("parameters", parametersType)
+ .invoke("get" + NameUtils.capitalize(parameter.source().getName()), createGradleProperty(parameter));
+ if (!parameter.required() && parameter.defaultValue() == null) {
+ expression = expression.invoke("getOrNull", parameter.type());
+ } else {
+ expression = expression.invoke("get", parameter.type());
+ }
+ if (parameter.source().getType().isAssignable(File.class)) {
+ expression = expression.invoke("getAsFile", TypeDef.of(File.class));
+ }
+ params.put(
+ parameter.source().getName(),
+ ModelUtils.convertParameterIfRequired(
+ parameter.source().getType(), parameter.source().getName() + "Param", statements, expression
+ )
+ );
+ }
+ statements.add(PluginUtils.executeTaskMethod(taskConfig.source(), taskConfig.methodName(), params));
+ return StatementDef.multi(statements);
+ }
+
+ static TypeDef createGradleProperty(ParameterConfig parameter) {
+ ClassElement type = parameter.source().getType();
+ if (type.isAssignable(File.class)) {
+ if (parameter.directory()) {
+ return ClassTypeDef.of("org.gradle.api.file.DirectoryProperty");
+ }
+ return ClassTypeDef.of("org.gradle.api.file.RegularFileProperty");
+ }
+ if (parameter.type() instanceof ClassTypeDef.Parameterized parameterized) {
+ if (type.isAssignable(Map.class)) {
+ return TypeDef.parameterized(
+ ClassTypeDef.of("org.gradle.api.provider.MapProperty"),
+ parameterized.typeArguments().get(0),
+ parameterized.typeArguments().get(1)
+ );
+ } else if (type.isAssignable(List.class)) {
+ return TypeDef.parameterized(
+ ClassTypeDef.of("org.gradle.api.provider.ListProperty"),
+ parameterized.typeArguments().get(0)
+ );
+ } else if (type.isAssignable(Set.class)) {
+ return TypeDef.parameterized(
+ ClassTypeDef.of("org.gradle.api.provider.SetProperty"),
+ parameterized.typeArguments().get(0)
+ );
+ }
+ }
+ return TypeDef.parameterized(
+ ClassTypeDef.of("org.gradle.api.provider.Property"),
+ parameter.type()
+ );
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTypeBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTypeBuilder.java
new file mode 100644
index 0000000..c409a99
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/gradle/builder/GradleTypeBuilder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.gradle.builder;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.sourcegen.annotations.GenerateGradlePlugin;
+import io.micronaut.sourcegen.generator.visitors.gradle.GradlePluginUtils;
+import io.micronaut.sourcegen.model.ObjectDef;
+
+import java.util.List;
+
+/**
+ * An interface for a Gradle plugin builder type.
+ */
+@Internal
+public interface GradleTypeBuilder {
+
+ /**
+ * Get the gradle type it can generate.
+ *
+ * @return The type
+ */
+ @NonNull GenerateGradlePlugin.Type getType();
+
+ /**
+ * Generate the gradle type.
+ *
+ * @param pluginConfig The configuration
+ * @return The generated objects for the type
+ */
+ @NonNull List build(@NonNull GradlePluginUtils.GradlePluginConfig pluginConfig);
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoBuilder.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoBuilder.java
new file mode 100644
index 0000000..ec585a9
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoBuilder.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.maven;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.naming.NameUtils;
+import io.micronaut.sourcegen.generator.visitors.ModelUtils;
+import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel;
+import io.micronaut.sourcegen.generator.visitors.PluginUtils;
+import io.micronaut.sourcegen.generator.visitors.maven.MavenPluginUtils.MavenTaskConfig;
+import io.micronaut.sourcegen.generator.visitors.PluginUtils.ParameterConfig;
+import io.micronaut.sourcegen.model.AnnotationDef;
+import io.micronaut.sourcegen.model.AnnotationDef.AnnotationDefBuilder;
+import io.micronaut.sourcegen.model.ClassDef;
+import io.micronaut.sourcegen.model.ClassDef.ClassDefBuilder;
+import io.micronaut.sourcegen.model.ClassTypeDef;
+import io.micronaut.sourcegen.model.ExpressionDef;
+import io.micronaut.sourcegen.model.FieldDef;
+import io.micronaut.sourcegen.model.MethodDef;
+import io.micronaut.sourcegen.model.StatementDef;
+import io.micronaut.sourcegen.model.TypeDef;
+import io.micronaut.sourcegen.model.VariableDef;
+
+import javax.lang.model.element.Modifier;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A builder for Maven Mojos.
+ */
+@Internal
+public class MavenMojoBuilder {
+
+ public static final String MOJO_SUFFIX = "Mojo";
+
+ /**
+ * Method for building the Maven mojo.
+ *
+ * @param taskConfig The config
+ * @return The class
+ */
+ public ClassDef build(MavenTaskConfig taskConfig) {
+ String mojoName = taskConfig.packageName() + "." + taskConfig.namePrefix() + MOJO_SUFFIX;
+ ClassDefBuilder builder = ClassDef.builder(mojoName)
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);
+ if (taskConfig.micronautPlugin()) {
+ builder.superclass(ClassTypeDef.of("io.micronaut.maven.AbstractMicronautMojo"));
+ } else {
+ builder.superclass(ClassTypeDef.of("org.apache.maven.plugin.AbstractMojo"));
+ }
+
+ for (ParameterConfig parameter : taskConfig.parameters()) {
+ addParameter(taskConfig, parameter, builder);
+ }
+
+ builder.addMethod(MethodDef.builder("isEnabled")
+ .addModifiers(Modifier.PROTECTED, Modifier.ABSTRACT)
+ .returns(TypeDef.of(boolean.class))
+ .addJavadoc("Determines if this mojo must be executed.\n@return true if the mojo is enabled")
+ .build()
+ );
+ builder.addMethods(taskConfig.generatedModels().stream().map(GeneratedModel::convertorMethod).toList());
+ builder.addMethod(createExecuteMethod(taskConfig));
+ builder.addJavadoc(taskConfig.taskJavadoc());
+
+ return builder.build();
+ }
+
+ private void addParameter(MavenTaskConfig taskConfig, ParameterConfig parameter, ClassDefBuilder builder) {
+ if (parameter.internal() || parameter.output()) {
+ builder.addMethod(MethodDef
+ .builder("get" + NameUtils.capitalize(parameter.source().getName()))
+ .returns(parameter.type())
+ .addModifiers(Modifier.PROTECTED, Modifier.ABSTRACT)
+ .addJavadoc(parameter.javadoc())
+ .build()
+ );
+ } else {
+ AnnotationDefBuilder ann = AnnotationDef.builder(ClassTypeDef.of("org.apache.maven.plugins.annotations.Parameter"));
+ if (parameter.defaultValue() != null) {
+ ann.addMember("defaultValue", parameter.defaultValue());
+ }
+ if (parameter.required()) {
+ ann.addMember("required", true);
+ }
+ if (parameter.globalProperty() != null) {
+ ann.addMember("property", taskConfig.mavenPropertyPrefix()
+ + "." + MavenPluginUtils.toDotSeparated(parameter.globalProperty()));
+ }
+ FieldDef field = FieldDef.builder(parameter.source().getName())
+ .ofType(parameter.type())
+ .addModifiers(Modifier.PROTECTED)
+ .addAnnotation(ann.build())
+ .addJavadoc(parameter.javadoc())
+ .build();
+ builder.addField(field);
+ }
+ }
+
+ private MethodDef createExecuteMethod(MavenTaskConfig taskConfig) {
+ return MethodDef.builder("execute")
+ .overrides()
+ .addModifiers(Modifier.PUBLIC)
+ .addJavadoc(taskConfig.methodJavadoc())
+ .build((t, params) -> t.invoke("isEnabled", TypeDef.of(boolean.class))
+ .ifFalse(
+ t.invoke("getLog", ClassTypeDef.of("org.apache.maven.plugin.logging.Log"))
+ .invoke("debug", TypeDef.VOID, ExpressionDef.constant(taskConfig.namePrefix() + MOJO_SUFFIX + " is disabled")),
+ runTask(taskConfig, t)
+ ));
+ }
+
+ private StatementDef runTask(MavenTaskConfig taskConfig, VariableDef.This t) {
+ Map params = new HashMap<>();
+ List statements = new ArrayList<>();
+ for (ParameterConfig parameter: taskConfig.parameters()) {
+ ExpressionDef expression;
+ if (parameter.internal() || parameter.output()) {
+ String getter = "get" + NameUtils.capitalize(parameter.source().getName());
+ expression = t.invoke(getter, parameter.type());
+ } else {
+ expression = t.field(parameter.source().getName(), parameter.type());
+ }
+ params.put(
+ parameter.source().getName(),
+ ModelUtils.convertParameterIfRequired(
+ parameter.source().getType(), parameter.source().getName() + "Param", statements, expression
+ )
+ );
+ }
+ statements.add(PluginUtils.executeTaskMethod(taskConfig.source(), taskConfig.methodName(), params));
+ return StatementDef.multi(statements);
+ }
+
+}
diff --git a/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoGenerationTriggerAnnotationVisitor.java b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoGenerationTriggerAnnotationVisitor.java
new file mode 100644
index 0000000..da57708
--- /dev/null
+++ b/micronaut-build-plugin-sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/maven/MavenMojoGenerationTriggerAnnotationVisitor.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.sourcegen.generator.visitors.maven;
+
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.inject.ast.ClassElement;
+import io.micronaut.inject.processing.ProcessingException;
+import io.micronaut.inject.visitor.TypeElementVisitor;
+import io.micronaut.inject.visitor.VisitorContext;
+import io.micronaut.sourcegen.annotations.GenerateMavenMojo;
+import io.micronaut.sourcegen.generator.SourceGenerator;
+import io.micronaut.sourcegen.generator.SourceGenerators;
+import io.micronaut.sourcegen.generator.visitors.ModelUtils.GeneratedModel;
+import io.micronaut.sourcegen.generator.visitors.maven.MavenPluginUtils.MavenTaskConfig;
+import io.micronaut.sourcegen.model.ObjectDef;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Visitor for generating maven mojos.
+ *
+ * @author Andriy Dmytruk
+ * @since 1.0.x
+ */
+@Internal
+public final class MavenMojoGenerationTriggerAnnotationVisitor implements TypeElementVisitor