From 15701efde6a31ec6c5dc324f404de0a5516e3acf Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Mon, 6 Apr 2026 09:37:12 -0700 Subject: [PATCH 1/6] Add a Gradle Plugin for codegen --- README.md | 67 +++-- examples/standalone-types/README.md | 9 + examples/standalone-types/build.gradle.kts | 37 +-- examples/standalone-types/gradle.properties | 2 + examples/standalone-types/settings.gradle.kts | 2 + gradle-plugin/README.md | 242 +++++++++++++++++ gradle-plugin/build.gradle.kts | 104 +++++++ gradle-plugin/settings.gradle.kts | 1 + .../gradle/SmithyBuildModesValueSource.java | 63 +++++ .../java/gradle/SmithyJavaExtension.java | 95 +++++++ .../smithy/java/gradle/SmithyJavaPlugin.java | 255 ++++++++++++++++++ .../smithy/java/gradle/SmithyJavaVersion.java | 51 ++++ .../gradle/tasks/MergeServiceFilesTask.java | 117 ++++++++ .../smithy/java/gradle/version.properties | 1 + mcp/mcp-schemas/build.gradle.kts | 98 +------ .../model-bundle-api/build.gradle.kts | 33 +-- .../META-INF/smithy => model}/bundle.smithy | 0 .../main/resources/META-INF/smithy/manifest | 1 - settings.gradle.kts | 1 + 19 files changed, 998 insertions(+), 181 deletions(-) create mode 100644 examples/standalone-types/gradle.properties create mode 100644 gradle-plugin/README.md create mode 100644 gradle-plugin/build.gradle.kts create mode 100644 gradle-plugin/settings.gradle.kts create mode 100644 gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java create mode 100644 gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java create mode 100644 gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java create mode 100644 gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaVersion.java create mode 100644 gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/tasks/MergeServiceFilesTask.java create mode 100644 gradle-plugin/src/main/resources/software/amazon/smithy/java/gradle/version.properties rename model-bundle/model-bundle-api/{src/main/resources/META-INF/smithy => model}/bundle.smithy (100%) delete mode 100644 model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/manifest diff --git a/README.md b/README.md index ad25ca94b2..dd452dceb5 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,48 @@ and building services. These examples can be used as a template for a new projec ### Codegen (Gradle) -To use the Smithy Java code generators with Gradle, first create a Smithy Gradle project. +The recommended way to use Smithy Java code generation with Gradle is the +[**Smithy Java Plugin**](./gradle-plugin/README.md), which handles dependency management, +source set wiring, and task ordering automatically: + +```kotlin +// build.gradle.kts +plugins { + id("software.amazon.smithy.java.gradle.smithy-java") version "" +} +``` + +Configure your [`smithy-build.json`](https://smithy.io/2.0/guides/smithy-build-json.html) with the +`java-codegen` plugin: + +```json +{ + "version": "1.0", + "plugins": { + "java-codegen": { + "service": "com.example#CoffeeShop", + "namespace": "software.amazon.smithy.java.examples", + "headerFile": "license.txt", + "modes": ["client"] + } + } +} +``` + +That's it, run `./gradlew build` and the plugin takes care of the rest. See the +[gradle-plugin README](./gradle-plugin/README.md) for full configuration options, +examples (types-only, client, server), and escape hatches. + +
+Manual setup (without the Gradle plugin) + +If you prefer manual control, apply [`smithy-base`](https://smithy.io/2.0/guides/gradle-plugin/index.html#smithy-gradle-plugins) +directly: > [!NOTE] > You can use the `smithy init` [CLI](https://smithy.io/2.0/guides/smithy-cli/index.html) command to create a new > Smithy Gradle project. The command `smithy init --template quickstart-gradle` will create a new basic Smithy Gradle project. -Then apply the [`smithy-base`](https://smithy.io/2.0/guides/gradle-plugin/index.html#smithy-gradle-plugins) gradle plugin to -your project. - ```diff // build.gradle.kts plugins { @@ -67,28 +100,8 @@ dependencies { } ``` -Now, configure your [`smithy-build`](https://smithy.io/2.0/guides/smithy-build-json.html) to use one of the -Smithy Java codegen plugins. For example, to generate a client for a `CoffeeShop` service we would -add the following to our `smithy-build.json`: - -```diff -// smithy-build.json -{ - ... - "plugins": { -+ "java-codegen": { -+ "service": "com.example#CoffeeShop", -+ "namespace": "software.amazon.smithy.java.examples", -+ "headerFile": "license.txt", -+ "modes": ["client"] -+ } - } -} -``` - -Your project is now configured to generate Java code from our model. To execute a build run the -gradle `build` task for your project. To compile the generated code as part of your project, -add the generated package to your `main` sourceSet. For example: +To compile the generated code as part of your project, +add the generated package to your `main` sourceSet: ```kotlin // build.gradle.kts @@ -114,6 +127,8 @@ tasks.named("compileJava") { } ``` +
+ ### Stability Classes and API's annotated with `@SmithyInternal` are for internal use by Smithy-Java libraries and should **not** be diff --git a/examples/standalone-types/README.md b/examples/standalone-types/README.md index 7c8a59a6bf..7c708eb53f 100644 --- a/examples/standalone-types/README.md +++ b/examples/standalone-types/README.md @@ -1,6 +1,15 @@ ## Examples: Standalone Types Package that generates Java types for a model without a service. +### Prerequisites +This example depends on the `software.amazon.smithy.java.gradle.smithy-java` Gradle plugin, which must be +available in a repository declared in `settings.gradle.kts`. If building from a local checkout +before release, publish the plugin to Maven Local first: + +```console +./gradlew -p gradle-plugin publishToMavenLocal +``` + ### Usage To use this example as a template, run the following command with the [Smithy CLI](https://smithy.io/2.0/guides/smithy-cli/index.html): diff --git a/examples/standalone-types/build.gradle.kts b/examples/standalone-types/build.gradle.kts index 444d3e9823..5d7adab3f9 100644 --- a/examples/standalone-types/build.gradle.kts +++ b/examples/standalone-types/build.gradle.kts @@ -1,47 +1,16 @@ - plugins { - `java-library` - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") } dependencies { - val smithyJavaVersion: String by project - - smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") - api("software.amazon.smithy.java:core:$smithyJavaVersion") - api("software.amazon.smithy.java:framework-errors:$smithyJavaVersion") - testImplementation("org.hamcrest:hamcrest:3.0") testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation("org.assertj:assertj-core:3.27.7") } -afterEvaluate { - val typesPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$typesPath/java") - } - resources { - srcDir("$typesPath/resources") - } - } - } -} - -tasks { - val smithyBuild by getting - compileJava { - dependsOn(smithyBuild) - } - processResources { - dependsOn(smithyBuild) - } - withType { - useJUnitPlatform() - } +tasks.withType { + useJUnitPlatform() } repositories { diff --git a/examples/standalone-types/gradle.properties b/examples/standalone-types/gradle.properties new file mode 100644 index 0000000000..0ce4688866 --- /dev/null +++ b/examples/standalone-types/gradle.properties @@ -0,0 +1,2 @@ +smithyJavaVersion=+ +smithyGradleVersion=1.4.0 diff --git a/examples/standalone-types/settings.gradle.kts b/examples/standalone-types/settings.gradle.kts index 28d9e8ec4f..48b876b827 100644 --- a/examples/standalone-types/settings.gradle.kts +++ b/examples/standalone-types/settings.gradle.kts @@ -4,9 +4,11 @@ pluginManagement { val smithyGradleVersion: String by settings + val smithyJavaVersion: String by settings plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) } repositories { diff --git a/gradle-plugin/README.md b/gradle-plugin/README.md new file mode 100644 index 0000000000..e76fbb3260 --- /dev/null +++ b/gradle-plugin/README.md @@ -0,0 +1,242 @@ +# Smithy Java Gradle Plugin + +A Gradle plugin that simplifies Java code generation from Smithy models. It replaces the manual +boilerplate of applying `smithy-base`, wiring source sets, managing `smithyBuild` dependencies, +and coordinating task ordering with a single plugin application. + +## Quick Start + +**Before** (manual setup): +```kotlin +plugins { + `java-library` + id("software.amazon.smithy.gradle.smithy-base") version "1.4.0" +} + +dependencies { + smithyBuild("software.amazon.smithy.java:codegen-plugin:") + smithyBuild("software.amazon.smithy.java:client-core:") + api("software.amazon.smithy.java:core:") + api("software.amazon.smithy.java:framework-errors:") + api("software.amazon.smithy.java:client-core:") +} + +afterEvaluate { + val path = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() + sourceSets { + main { + java { srcDir("$path/java") } + resources { srcDir("$path/resources") } + } + } +} + +tasks.compileJava { dependsOn("smithyBuild") } +tasks.processResources { dependsOn("smithyBuild") } +``` + +**After** (with this plugin): +```kotlin +plugins { + id("software.amazon.smithy.java.gradle.smithy-java") version "" +} +``` + +That's it. The plugin handles `java-library`, `smithy-base`, dependency management, source set +wiring, and task ordering automatically. + +## Installation + +Add the plugin to your `settings.gradle.kts`: + +```kotlin +pluginManagement { + plugins { + id("software.amazon.smithy.java.gradle.smithy-java") version "" + } + repositories { + mavenCentral() + gradlePluginPortal() + } +} +``` + +Then apply it in your `build.gradle.kts`: + +```kotlin +plugins { + id("software.amazon.smithy.java.gradle.smithy-java") +} +``` + +## Examples + +### Types Only + +The simplest case, generate data types from a Smithy model with no client or server runtime. + +`smithy-build.json`: +```json +{ + "version": "1.0", + "plugins": { + "java-codegen": { + "namespace": "com.example.types", + "headerFile": "license.txt", + "modes": ["types"] + } + } +} +``` + +`build.gradle.kts`: +```kotlin +plugins { + id("software.amazon.smithy.java.gradle.smithy-java") +} +``` + +No additional configuration needed, the plugin adds the correct runtime dependencies automatically. + +### Client + +Generate a client for a service. + +`smithy-build.json`: +```json +{ + "version": "1.0", + "plugins": { + "java-codegen": { + "service": "com.example#MyService", + "namespace": "com.example.client", + "headerFile": "license.txt", + "modes": ["client"] + } + } +} +``` + +`build.gradle.kts`: +```kotlin +plugins { + id("software.amazon.smithy.java.gradle.smithy-java") +} + +dependencies { + // Add the protocol and transport your service uses + api("software.amazon.smithy.java:aws-client-restjson:") +} +``` + +You only need to declare protocol/transport dependencies specific to your service, the plugin +handles the rest. + +### Server + +Generate server stubs. + +`smithy-build.json`: +```json +{ + "version": "1.0", + "plugins": { + "java-codegen": { + "service": "com.example#MyService", + "namespace": "com.example.server", + "headerFile": "license.txt", + "modes": ["server"] + } + } +} +``` + +`build.gradle.kts`: +```kotlin +plugins { + id("software.amazon.smithy.java.gradle.smithy-java") +} + +dependencies { + // Server runtime and protocol + implementation("software.amazon.smithy.java:server-netty:") + implementation("software.amazon.smithy.java:aws-server-restjson:") +} +``` + +The plugin handles codegen and runtime dependencies automatically, you only need to declare +the server runtime and protocol implementation you want to use. + +## Configuration + +All configuration is optional. The plugin works out of the box for standard projects. + +```kotlin +smithyJava { + // Explicit mode declaration, overrides smithy-build.json inference. + // Valid values: "types", "client", "server" + modes.add("client") + + // Compile output from additional smithy-build.json plugins. + generatedPluginOutputs.add("trait-codegen") + + // Disable automatic dependency management to control all deps manually. + autoAddDependencies = false +} +``` + +### `modes` + +Explicitly declares which codegen modes the project uses (`"types"`, `"client"`, `"server"`). +When set, these override whatever is in `smithy-build.json` for dependency resolution purposes. + +When empty (default), modes are read from `smithy-build.json` automatically. + +### `generatedPluginOutputs` + +When your `smithy-build.json` has plugins beyond `java-codegen` that produce Java source +(e.g. `trait-codegen`), list them here so their output gets compiled: + +```kotlin +smithyJava { + generatedPluginOutputs.add("trait-codegen") +} +``` + +Service files from multiple plugins are merged automatically. Disable with +`mergeServiceFiles = false` if not needed. + +### `autoAddDependencies` + +When `true` (default), the plugin automatically adds the correct Smithy Java runtime +dependencies based on the active modes. Set to `false` when you need full control over +dependency versions or want to use project dependencies (e.g. in a monorepo): + +```kotlin +smithyJava { + autoAddDependencies = false +} + +dependencies { + smithyBuild("software.amazon.smithy.java:codegen-plugin:") + api("software.amazon.smithy.java:core:") + // ... full control +} +``` + +## Requirements + +- Java 21 or later +- Gradle 8.5 or later (first version to support Java 21 runtime) +- A `smithy-build.json` with a `java-codegen` plugin configured + +## Custom Source Projection + +If you use a non-default projection, configure it via the `smithy` extension (provided by +`smithy-base`): + +```kotlin +smithy { + sourceProjection = "custom-projection" +} +``` diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts new file mode 100644 index 0000000000..193558a01c --- /dev/null +++ b/gradle-plugin/build.gradle.kts @@ -0,0 +1,104 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + `java-gradle-plugin` + `java-library` + `maven-publish` + signing +} + +val smithyJavaVersion = file("../VERSION").readText().trim() + +group = "software.amazon.smithy.java" +version = smithyJavaVersion +description = "Gradle plugin for Smithy Java code generation" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + withSourcesJar() + withJavadocJar() +} + +tasks.withType { + options.encoding = "UTF-8" + options.release.set(21) +} + +repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() +} + +// Versions are pinned here because this module is an included build (standalone settings.gradle.kts) +// and cannot access the parent project's version catalog. Update these when bumping smithy versions. +dependencies { + implementation("software.amazon.smithy.gradle:smithy-base:1.4.0") + implementation("software.amazon.smithy:smithy-model:1.68.0") +} + +gradlePlugin { + plugins { + create("smithy-java") { + id = "software.amazon.smithy.java.gradle.smithy-java" + displayName = "Smithy Java Plugin" + description = project.description + implementationClass = "software.amazon.smithy.java.gradle.SmithyJavaPlugin" + tags.addAll("smithy", "codegen", "java") + } + } +} + +tasks.withType { + filter("tokens" to mapOf("SmithyJavaVersion" to version.toString())) +} + +publishing { + repositories { + maven { + name = "stagingRepository" + url = file("../build/staging").toURI() + } + } + publications.withType { + pom { + name.set("Smithy :: Java :: Gradle Plugin") + description.set(project.description) + url.set("https://github.com/smithy-lang/smithy-java") + licenses { + license { + name.set("Apache License 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + developers { + developer { + id.set("smithy") + name.set("Smithy") + organization.set("Amazon Web Services") + organizationUrl.set("https://aws.amazon.com") + roles.add("developer") + } + } + scm { + url.set("https://github.com/smithy-lang/smithy-java.git") + } + } + } +} + +signing { + setRequired { + gradle.taskGraph.allTasks.any { it is PublishToMavenRepository } + } + if (project.hasProperty("signingKey") && project.hasProperty("signingPassword")) { + useInMemoryPgpKeys( + project.properties["signingKey"].toString(), + project.properties["signingPassword"].toString(), + ) + sign(publishing.publications) + } +} diff --git a/gradle-plugin/settings.gradle.kts b/gradle-plugin/settings.gradle.kts new file mode 100644 index 0000000000..ee6b171fa5 --- /dev/null +++ b/gradle-plugin/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "gradle-plugin" diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java new file mode 100644 index 0000000000..3228b76bbc --- /dev/null +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.gradle; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashSet; +import java.util.Set; +import org.gradle.api.provider.SetProperty; +import org.gradle.api.provider.ValueSource; +import org.gradle.api.provider.ValueSourceParameters; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; + +/** + * A Gradle ValueSource that reads codegen modes from smithy-build.json files. + * + *

Using a ValueSource ensures Gradle's configuration cache properly tracks + * the smithy-build.json file as a configuration input and invalidates the cache + * when the file's modes change. + */ +public abstract class SmithyBuildModesValueSource implements ValueSource, SmithyBuildModesValueSource.Params> { + + private static final String JAVA_CODEGEN_PLUGIN_NAME = "java-codegen"; + + public interface Params extends ValueSourceParameters { + SetProperty getSmithyBuildConfigs(); + } + + @Override + public Set obtain() { + for (File config : getParameters().getSmithyBuildConfigs().get()) { + if (!config.exists()) { + continue; + } + try { + String content = Files.readString(config.toPath()); + ObjectNode root = Node.parseJsonWithComments(content).expectObjectNode(); + return root.getObjectMember("plugins") + .flatMap(plugins -> plugins.getObjectMember(JAVA_CODEGEN_PLUGIN_NAME)) + .flatMap(codegen -> codegen.getArrayMember("modes")) + .map(SmithyBuildModesValueSource::extractModes) + .orElse(Set.of("types")); + } catch (IOException ignored) { + } + } + return Set.of("types"); + } + + private static Set extractModes(ArrayNode modesArray) { + Set modes = new HashSet<>(); + for (Node element : modesArray.getElements()) { + element.asStringNode().map(StringNode::getValue).ifPresent(modes::add); + } + return modes; + } +} diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java new file mode 100644 index 0000000000..ae657056f2 --- /dev/null +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java @@ -0,0 +1,95 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.gradle; + +import java.util.Collections; + +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; + +/** + * DSL extension for the smithy-java Gradle plugin. + * + *

Configure via the {@code smithyJava} block: + *

{@code
+ * smithyJava {
+ *     modes.add("client")                          // explicit mode declaration
+ *     generatedPluginOutputs.add("trait-codegen")  // wire additional plugin output dirs
+ *     mergeServiceFiles = true                     // default
+ * }
+ * }
+ */ +public abstract class SmithyJavaExtension { + + public SmithyJavaExtension() { + getAutoAddDependencies().convention(true); + getModes().convention(Collections.emptySet()); + getGeneratedPluginOutputs().convention(Collections.emptyList()); + getMergeServiceFiles().convention(true); + } + + /** + * Whether to automatically add dependencies based on the codegen modes. + * When enabled (default), the plugin adds the appropriate dependencies: + * + *
    + *
  • Always: {@code codegen-plugin} to smithyBuild; {@code core} and + * {@code framework-errors} to api
  • + *
  • Client mode: {@code client-core} to smithyBuild and api
  • + *
  • Server mode: {@code server-api} to smithyBuild and api
  • + *
+ * + *

Set to {@code false} to manage all dependencies manually. + * + * @return property controlling auto-dependency management + */ + public abstract Property getAutoAddDependencies(); + + /** + * Codegen modes to use for dependency resolution. When non-empty, these take + * precedence over modes inferred from {@code smithy-build.json}. Valid values + * are {@code "types"}, {@code "client"}, and {@code "server"}. + * + *

Example: + *

{@code
+     * smithyJava {
+     *     modes.add("client")
+     *     modes.add("server")
+     * }
+     * }
+ * + *

When empty (default), modes are inferred by parsing the {@code java-codegen} + * plugin configuration in {@code smithy-build.json}. + * + * @return set of explicitly declared codegen modes + */ + public abstract SetProperty getModes(); + + /** + * Additional Smithy build plugin names (as declared in {@code smithy-build.json}) + * whose generated output directories should be wired into the Java source set. + * The {@code java-codegen} plugin output is always wired automatically. + * + *

For example, add {@code "trait-codegen"} if your {@code smithy-build.json} + * uses the trait-codegen plugin alongside java-codegen and you want its output + * compiled as part of this project. + * + * @return list of additional plugin output names to include + */ + public abstract ListProperty getGeneratedPluginOutputs(); + + /** + * Whether to automatically merge {@code META-INF/services} files when + * {@link #getGeneratedPluginOutputs()} is non-empty. Defaults to {@code true}. + * + *

When multiple Smithy build plugins produce service provider files, they + * may conflict. This option enables a merge task that combines them. + * + * @return property controlling service file merging + */ + public abstract Property getMergeServiceFiles(); +} diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java new file mode 100644 index 0000000000..fdf0c63758 --- /dev/null +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java @@ -0,0 +1,255 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.gradle; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.file.FileCollection; +import org.gradle.api.plugins.JavaLibraryPlugin; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.tasks.Jar; +import org.gradle.language.jvm.tasks.ProcessResources; +import software.amazon.smithy.gradle.SmithyExtension; +import software.amazon.smithy.java.gradle.tasks.MergeServiceFilesTask; + +/** + * Gradle plugin that simplifies Java code generation from Smithy models. + * + *

This plugin applies {@code java-library} and + * {@code software.amazon.smithy.gradle.smithy-base}, then automatically: + *

    + *
  • Parses {@code smithy-build.json} to determine codegen modes
  • + *
  • Adds required dependencies based on detected modes
  • + *
  • Wires generated source and resource directories into the main source set
  • + *
  • Sets up task dependencies (compileJava, processResources, sourcesJar)
  • + *
  • Optionally merges META-INF/services files from multiple plugin outputs
  • + *
+ */ +public class SmithyJavaPlugin implements Plugin { + + private static final String SMITHY_JAVA_GROUP = "software.amazon.smithy.java"; + private static final String JAVA_CODEGEN_PLUGIN_NAME = "java-codegen"; + private static final String SMITHY_BUILD_TASK_NAME = "smithyBuild"; + private static final String MERGE_SERVICE_FILES_TASK_NAME = "mergeSmithyServiceFiles"; + + @Override + public void apply(Project project) { + project.getPlugins().apply(JavaLibraryPlugin.class); + project.getPlugins().apply("software.amazon.smithy.gradle.smithy-base"); + + SmithyJavaExtension ext = project.getExtensions() + .create("smithyJava", SmithyJavaExtension.class); + + SmithyExtension smithyExt = project.getExtensions() + .getByType(SmithyExtension.class); + + configureDependencies(project, smithyExt, ext); + wireGeneratedSources(project, smithyExt, ext); + configureTaskDependencies(project); + configureServiceFileMerging(project, smithyExt, ext); + } + + private void configureDependencies( + Project project, + SmithyExtension smithyExt, + SmithyJavaExtension ext + ) { + Configuration smithyBuild = project.getConfigurations().getByName("smithyBuild"); + Configuration api = project.getConfigurations().getByName("api"); + + // Resolve modes via ValueSource for configuration cache compatibility. + // If explicit modes are set in the DSL, use those directly; otherwise + // read smithy-build.json through a tracked ValueSource. + Provider> configFiles = smithyExt.getSmithyBuildConfigs() + .map(FileCollection::getFiles); + Provider> inferredModes = project.getProviders().of( + SmithyBuildModesValueSource.class, + spec -> spec.getParameters().getSmithyBuildConfigs().set(configFiles)); + Provider> modes = ext.getModes().map(declared -> + declared.isEmpty() ? inferredModes.get() : declared); + + smithyBuild.withDependencies(deps -> { + if (!ext.getAutoAddDependencies().getOrElse(true)) { + return; + } + String version = SmithyJavaVersion.VERSION; + Set resolved = modes.get(); + addIfAbsent(deps, project.getDependencies(), "codegen-plugin", version); + if (resolved.contains("client")) { + addIfAbsent(deps, project.getDependencies(), "client-core", version); + } + if (resolved.contains("server")) { + addIfAbsent(deps, project.getDependencies(), "server-api", version); + } + }); + + api.withDependencies(deps -> { + if (!ext.getAutoAddDependencies().getOrElse(true)) { + return; + } + String version = SmithyJavaVersion.VERSION; + Set resolved = modes.get(); + addIfAbsent(deps, project.getDependencies(), "core", version); + addIfAbsent(deps, project.getDependencies(), "framework-errors", version); + if (resolved.contains("client")) { + addIfAbsent(deps, project.getDependencies(), "client-core", version); + } + if (resolved.contains("server")) { + addIfAbsent(deps, project.getDependencies(), "server-api", version); + } + }); + } + + private void wireGeneratedSources( + Project project, + SmithyExtension smithyExt, + SmithyJavaExtension ext + ) { + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + sourceSets.named(SourceSet.MAIN_SOURCE_SET_NAME, sourceSet -> { + Provider projection = smithyExt.getSourceProjection(); + + Provider codegenPath = projection.flatMap( + p -> smithyExt.getPluginProjectionPath(p, JAVA_CODEGEN_PLUGIN_NAME)); + sourceSet.getJava().srcDir(codegenPath.map(p -> p.resolve("java").toFile())); + sourceSet.getResources().srcDir(codegenPath.map(p -> p.resolve("resources").toFile())); + + // Callable defers evaluation until the source set is resolved, so generatedPluginOutputs is finalized + sourceSet.getJava().srcDir(project.files((Callable) () -> + ext.getGeneratedPluginOutputs().get().stream() + .map(name -> smithyExt.getPluginProjectionPath( + projection.get(), name).get().toFile()) + .collect(Collectors.toList()))); + + sourceSet.getResources().srcDir(project.files((Callable) () -> + ext.getGeneratedPluginOutputs().get().stream() + .map(name -> smithyExt.getPluginProjectionPath( + projection.get(), name).get().toFile()) + .collect(Collectors.toList()))); + + // trait-codegen mixes .java and resource files in the same output directory + sourceSet.getResources().exclude("**/*.java"); + }); + } + + private void configureTaskDependencies(Project project) { + project.getTasks().named("compileJava", task -> task.dependsOn(SMITHY_BUILD_TASK_NAME)); + project.getTasks().named("processResources", task -> task.dependsOn(SMITHY_BUILD_TASK_NAME)); + + project.getTasks().withType(Jar.class).configureEach(jar -> { + if ("sourcesJar".equals(jar.getName())) { + jar.mustRunAfter(project.getTasks().named("compileJava")); + jar.dependsOn(SMITHY_BUILD_TASK_NAME); + } + }); + } + + private void configureServiceFileMerging( + Project project, + SmithyExtension smithyExt, + SmithyJavaExtension ext + ) { + Provider projection = smithyExt.getSourceProjection(); + + Provider codegenPath = projection.flatMap( + p -> smithyExt.getPluginProjectionPath(p, JAVA_CODEGEN_PLUGIN_NAME)); + Provider codegenServicesDir = codegenPath.map( + p -> p.resolve("resources/META-INF/services").toFile()); + + TaskProvider mergeTask = project.getTasks() + .register(MERGE_SERVICE_FILES_TASK_NAME, MergeServiceFilesTask.class, task -> { + task.dependsOn(SMITHY_BUILD_TASK_NAME); + task.setGroup("smithy"); + task.getServiceDirectories().from(codegenServicesDir); + + task.getServiceDirectories().from(project.files((Callable) () -> + ext.getGeneratedPluginOutputs().get().stream() + .map(name -> smithyExt.getPluginProjectionPath( + projection.get(), name) + .get() + .resolve("META-INF/services") + .toFile()) + .collect(Collectors.toList()))); + + task.onlyIf(t -> !ext.getGeneratedPluginOutputs() + .getOrElse(List.of()).isEmpty() + && ext.getMergeServiceFiles().getOrElse(true)); + }); + + Provider mergedServicesDir = project.getLayout().getBuildDirectory() + .dir("merged-services").map(d -> d.getAsFile()); + String mergedPathSegment = "merged-services" + File.separator; + + project.getTasks().named("processResources", ProcessResources.class, task -> { + task.dependsOn(mergeTask); + task.from(project.files((Callable) () -> { + if (isMergingActive(ext)) { + return mergedServicesDir.get(); + } + return List.of(); + }), spec -> spec.into(".")); + // Files added via from() above bypass eachFile, so only originals are excluded + task.eachFile(details -> { + if (isMergingActive(ext) + && details.getRelativePath().getPathString().startsWith("META-INF/services/") + && !details.getFile().getPath().contains(mergedPathSegment)) { + details.exclude(); + } + }); + }); + + project.getTasks().withType(Jar.class).configureEach(jar -> { + if ("sourcesJar".equals(jar.getName())) { + jar.dependsOn(mergeTask); + jar.from(project.files((Callable) () -> { + if (isMergingActive(ext)) { + return mergedServicesDir.get(); + } + return List.of(); + }), spec -> spec.into(".")); + jar.eachFile(details -> { + if (isMergingActive(ext) + && details.getRelativePath().getPathString().startsWith("META-INF/services/") + && !details.getFile().getPath().contains(mergedPathSegment)) { + details.exclude(); + } + }); + } + }); + } + + private static boolean isMergingActive(SmithyJavaExtension ext) { + return !ext.getGeneratedPluginOutputs().getOrElse(List.of()).isEmpty() + && ext.getMergeServiceFiles().getOrElse(true); + } + + private static void addIfAbsent( + DependencySet deps, + DependencyHandler handler, + String artifactName, + String version + ) { + boolean alreadyPresent = deps.stream() + .anyMatch(d -> SMITHY_JAVA_GROUP.equals(d.getGroup()) + && artifactName.equals(d.getName())); + if (!alreadyPresent) { + deps.add(handler.create(SMITHY_JAVA_GROUP + ":" + artifactName + ":" + version)); + } + } +} diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaVersion.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaVersion.java new file mode 100644 index 0000000000..919b4e59a7 --- /dev/null +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaVersion.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.gradle; + +import static java.lang.String.format; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import org.gradle.api.GradleException; + +/** + * Represents the smithy-java Gradle plugin version. + * + *

The version is read from a {@code version.properties} resource file that is stamped + * at build time with the current smithy-java version. + */ +public final class SmithyJavaVersion { + public static final String VERSION_OVERRIDE_VAR = "smithyjava.gradle.version.override"; + public static final String VERSION = resolveVersion(); + + private static final String VERSION_RESOURCE_NAME = "version.properties"; + private static final String VERSION_NUMBER_PROPERTY = "version"; + + private SmithyJavaVersion() {} + + private static String resolveVersion() { + try (InputStream inputStream = SmithyJavaVersion.class.getResourceAsStream(VERSION_RESOURCE_NAME)) { + if (inputStream == null) { + throw new GradleException(format("Version file '%s' not found.", VERSION_RESOURCE_NAME)); + } + Properties properties = new Properties(); + properties.load(inputStream); + String version = properties.get(VERSION_NUMBER_PROPERTY).toString(); + + String overrideVersion = System.getProperty(VERSION_OVERRIDE_VAR); + if (overrideVersion == null) { + overrideVersion = System.getenv(VERSION_OVERRIDE_VAR); + } + if (overrideVersion != null) { + return overrideVersion; + } + return version; + } catch (IOException e) { + throw new GradleException(format("Failed to read version file '%s'", VERSION_RESOURCE_NAME), e); + } + } +} diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/tasks/MergeServiceFilesTask.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/tasks/MergeServiceFilesTask.java new file mode 100644 index 0000000000..dd2e44540c --- /dev/null +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/tasks/MergeServiceFilesTask.java @@ -0,0 +1,117 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.gradle.tasks; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import javax.inject.Inject; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; + +/** + * Merges {@code META-INF/services} files from multiple Smithy build plugin outputs. + * + *

When multiple plugins (e.g., java-codegen and trait-codegen) each produce + * service provider configuration files, they must be merged to avoid one overwriting + * the other. This task collects all service files, groups them by service interface + * name, deduplicates entries, sorts them, and writes merged files to a single output + * directory. + */ +@CacheableTask +public abstract class MergeServiceFilesTask extends DefaultTask { + + @Inject + public MergeServiceFilesTask(ProjectLayout layout) { + getOutputDirectory().convention( + layout.getBuildDirectory().dir("merged-services/META-INF/services")); + setDescription("Merges META-INF/services files from multiple Smithy build plugins."); + } + + /** + * Directories containing {@code META-INF/services} files to merge. + * + * @return the file collection of service directories + */ + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract ConfigurableFileCollection getServiceDirectories(); + + /** + * Output directory for merged service files. + * + * @return the output directory property + */ + @OutputDirectory + public abstract DirectoryProperty getOutputDirectory(); + + @TaskAction + public void merge() { + Map> entries = new TreeMap<>(); + + for (File serviceDir : getServiceDirectories()) { + if (!serviceDir.exists() || !serviceDir.isDirectory()) { + continue; + } + File[] files = serviceDir.listFiles(); + if (files == null) { + continue; + } + + for (File serviceFile : files) { + if (!serviceFile.isFile()) { + continue; + } + try { + Set lines = entries.computeIfAbsent( + serviceFile.getName(), + k -> new TreeSet<>()); + Files.readAllLines(serviceFile.toPath()) + .stream() + .map(String::trim) + .filter(l -> !l.isEmpty() && !l.startsWith("#")) + .forEach(lines::add); + } catch (IOException e) { + throw new GradleException( + "Failed to read service file: " + serviceFile, + e); + } + } + } + + File outputDir = getOutputDirectory().getAsFile().get(); + if (!outputDir.mkdirs() && !outputDir.isDirectory()) { + throw new GradleException("Failed to create output directory: " + outputDir); + } + + entries.forEach((name, lines) -> { + Path outFile = outputDir.toPath().resolve(name); + try { + Files.writeString(outFile, + String.join("\n", lines) + "\n"); + } catch (IOException e) { + throw new GradleException( + "Failed to write merged service file: " + outFile, + e); + } + }); + } + +} diff --git a/gradle-plugin/src/main/resources/software/amazon/smithy/java/gradle/version.properties b/gradle-plugin/src/main/resources/software/amazon/smithy/java/gradle/version.properties new file mode 100644 index 0000000000..1a502e195f --- /dev/null +++ b/gradle-plugin/src/main/resources/software/amazon/smithy/java/gradle/version.properties @@ -0,0 +1 @@ +version=@SmithyJavaVersion@ diff --git a/mcp/mcp-schemas/build.gradle.kts b/mcp/mcp-schemas/build.gradle.kts index 34be5a6571..1c3b2ed9b1 100644 --- a/mcp/mcp-schemas/build.gradle.kts +++ b/mcp/mcp-schemas/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("smithy-java.module-conventions") + id("software.amazon.smithy.java.gradle.smithy-java") alias(libs.plugins.smithy.gradle.jar) } @@ -8,6 +9,11 @@ description = "This module provides a schemas for MCP integration" extra["displayName"] = "Smithy :: Java :: MCP Schemas" extra["moduleName"] = "software.amazon.smithy.mcp.schemas" +smithyJava { + autoAddDependencies = false + generatedPluginOutputs.add("trait-codegen") +} + dependencies { api(project(":core")) api(libs.smithy.model) @@ -19,98 +25,8 @@ dependencies { smithyBuild(libs.smithy.traitcodegen) } -sourceSets { - main { - java { - srcDirs("model", "src/main/smithy") - } - } -} - -afterEvaluate { - val codegenPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - val traitsPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "trait-codegen") - sourceSets { - main { - java { - srcDir("$codegenPath/java") - srcDir(traitsPath) - } - resources { - srcDir("$codegenPath/resources") - srcDir(traitsPath) - exclude("**/*.java") // This is still required because of trait-codegen. - exclude("resources/META-INF/services/**") // Exclude original service files, use merged ones instead - } - - smithy { - srcDir("$traitsPath/model") - } - } - } -} - -tasks.named("compileJava") { - dependsOn("smithyBuild") -} - -// TODO remove once we move to codegen modes instead of plugins. -val serviceFilesMerger = - tasks.register("mergeServiceFiles") { - dependsOn(tasks.smithyBuild) - - val outputServiceDir = layout.buildDirectory.dir("merged-services/META-INF/services") - outputs.dir(outputServiceDir) - - val projectDir = project.projectDir - - doLast { - // Use hardcoded paths because of https://docs.gradle.org/8.14.3/userguide/configuration_cache.html#config_cache:requirements:disallowed_types - val sourceServiceDirs = - listOf( - File(projectDir, "build/smithyprojections/mcp-schemas/source/java-codegen/resources/META-INF/services"), - File(projectDir, "build/smithyprojections/mcp-schemas/source/trait-codegen/META-INF/services"), - ) - - val serviceEntries = mutableMapOf>() - - sourceServiceDirs.forEach { serviceDir -> - if (serviceDir.exists() && serviceDir.isDirectory) { - serviceDir.listFiles()?.forEach { serviceFile -> - if (serviceFile.isFile) { - val serviceName = serviceFile.name - serviceEntries - .computeIfAbsent(serviceName) { mutableSetOf() } - .addAll(serviceFile.readLines().map { it.trim() }) - } - } - } - } - - val outputDir = outputServiceDir.get().asFile - outputDir.mkdirs() - - serviceEntries.forEach { (serviceName, lines) -> - val serviceFile = File(outputDir, serviceName) - serviceFile.writeText(lines.sorted().joinToString("\n") + "\n") - } - } - } - -// processResources will include merged service files in the main resources -tasks.processResources { - dependsOn(serviceFilesMerger) - from(layout.buildDirectory.dir("merged-services")) { - into(".") - } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} - -// Ensure sourcesJar waits for smithyBuild to complete and includes merged so ervice files tasks.sourcesJar { - dependsOn(tasks.smithyBuild, serviceFilesMerger, "smithyJarStaging") - from(layout.buildDirectory.dir("merged-services")) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE + dependsOn("smithyJarStaging") } tasks.jar { diff --git a/model-bundle/model-bundle-api/build.gradle.kts b/model-bundle/model-bundle-api/build.gradle.kts index b31688634f..e033516e23 100644 --- a/model-bundle/model-bundle-api/build.gradle.kts +++ b/model-bundle/model-bundle-api/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application id("smithy-java.module-conventions") - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") } description = "This module implements the model-bundle utility" @@ -9,8 +9,10 @@ description = "This module implements the model-bundle utility" extra["displayName"] = "Smithy :: Java :: Model Bundle" extra["moduleName"] = "software.amazon.smithy.java.modelbundle.api" +smithyJava { +} + dependencies { - smithyBuild(project(":codegen:codegen-plugin")) implementation(project(":core")) implementation(project(":logging")) @@ -21,30 +23,3 @@ dependencies { api(project(":server:server-api")) api(project(":server:server-proxy")) } - -afterEvaluate { - val typesPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$typesPath/java") - } - resources { - srcDir("$typesPath/resources") - } - } - } -} - -tasks.named("compileJava") { - dependsOn("smithyBuild") -} - -// Needed because sources-jar needs to run after smithy-build is done -tasks.sourcesJar { - mustRunAfter(tasks.compileJava) -} - -tasks.processResources { - dependsOn(tasks.compileJava) -} diff --git a/model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/bundle.smithy b/model-bundle/model-bundle-api/model/bundle.smithy similarity index 100% rename from model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/bundle.smithy rename to model-bundle/model-bundle-api/model/bundle.smithy diff --git a/model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/manifest b/model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/manifest deleted file mode 100644 index b20775b863..0000000000 --- a/model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/manifest +++ /dev/null @@ -1 +0,0 @@ -bundle.smithy \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 837b715aa1..82dd658fb6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ //This ensures composite builds also have the repositories configured. pluginManagement { + includeBuild("gradle-plugin") repositories { mavenLocal() mavenCentral() From 547425a6b50b5aad07e0691014a5c6cdd852646c Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Wed, 10 Jun 2026 14:40:01 -0700 Subject: [PATCH 2/6] Address some PR comments --- gradle-plugin/build.gradle.kts | 2 +- .../amazon/smithy/java/gradle/SmithyJavaPlugin.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index 193558a01c..ed826f0654 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -36,7 +36,7 @@ repositories { // and cannot access the parent project's version catalog. Update these when bumping smithy versions. dependencies { implementation("software.amazon.smithy.gradle:smithy-base:1.4.0") - implementation("software.amazon.smithy:smithy-model:1.68.0") + implementation("software.amazon.smithy:smithy-model:1.71.0") } gradlePlugin { diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java index fdf0c63758..1ea1226d91 100644 --- a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java @@ -20,6 +20,7 @@ import org.gradle.api.file.FileCollection; import org.gradle.api.plugins.JavaLibraryPlugin; import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Delete; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskProvider; @@ -46,6 +47,7 @@ public class SmithyJavaPlugin implements Plugin { private static final String SMITHY_JAVA_GROUP = "software.amazon.smithy.java"; private static final String JAVA_CODEGEN_PLUGIN_NAME = "java-codegen"; private static final String SMITHY_BUILD_TASK_NAME = "smithyBuild"; + private static final String CLEAN_SMITHY_OUTPUT_TASK_NAME = "cleanSmithyOutput"; private static final String MERGE_SERVICE_FILES_TASK_NAME = "mergeSmithyServiceFiles"; @Override @@ -61,6 +63,7 @@ public void apply(Project project) { configureDependencies(project, smithyExt, ext); wireGeneratedSources(project, smithyExt, ext); + configureCleanOutput(project, smithyExt); configureTaskDependencies(project); configureServiceFileMerging(project, smithyExt, ext); } @@ -148,6 +151,16 @@ private void wireGeneratedSources( }); } + private void configureCleanOutput(Project project, SmithyExtension smithyExt) { + project.getTasks().register(CLEAN_SMITHY_OUTPUT_TASK_NAME, Delete.class, task -> { + task.setGroup("smithy"); + task.setDescription("Cleans the Smithy output directory before code generation"); + task.delete(smithyExt.getOutputDirectory()); + }); + project.getTasks().named(SMITHY_BUILD_TASK_NAME, task -> + task.dependsOn(CLEAN_SMITHY_OUTPUT_TASK_NAME)); + } + private void configureTaskDependencies(Project project) { project.getTasks().named("compileJava", task -> task.dependsOn(SMITHY_BUILD_TASK_NAME)); project.getTasks().named("processResources", task -> task.dependsOn(SMITHY_BUILD_TASK_NAME)); From f461ecd04a03bd0cc4700d741ed29b5f03b66516 Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Wed, 10 Jun 2026 15:47:59 -0700 Subject: [PATCH 3/6] Migrate all examples to the new gradle plugin --- examples/basic-server/build.gradle.kts | 24 +---------- examples/basic-server/settings.gradle.kts | 2 + examples/end-to-end/build.gradle.kts | 41 +++---------------- examples/end-to-end/gradle.properties | 2 + examples/end-to-end/settings.gradle.kts | 2 + .../event-streaming-client/build.gradle.kts | 26 +++--------- .../settings.gradle.kts | 2 + examples/lambda/build.gradle.kts | 24 +---------- examples/lambda/settings.gradle.kts | 2 + examples/mcp-server/build.gradle.kts | 23 +---------- examples/mcp-server/settings.gradle.kts | 2 + examples/restjson-client/build.gradle.kts | 27 +++--------- examples/restjson-client/settings.gradle.kts | 2 + .../build.gradle.kts | 34 +++------------ .../settings.gradle.kts | 2 + 15 files changed, 39 insertions(+), 176 deletions(-) create mode 100644 examples/end-to-end/gradle.properties diff --git a/examples/basic-server/build.gradle.kts b/examples/basic-server/build.gradle.kts index 8ba583a27d..7d388c6efe 100644 --- a/examples/basic-server/build.gradle.kts +++ b/examples/basic-server/build.gradle.kts @@ -1,15 +1,11 @@ plugins { - `java-library` - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") application } dependencies { val smithyJavaVersion: String by project - smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") - smithyBuild("software.amazon.smithy.java:server-api:$smithyJavaVersion") - implementation("software.amazon.smithy.java:server-netty:$smithyJavaVersion") implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") } @@ -19,24 +15,6 @@ application { mainClass = "software.amazon.smithy.java.server.example.BasicServerExample" } -// Add generated Java files to the main sourceSet -afterEvaluate { - val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$serverPath/java") - } - } - } -} - -tasks { - compileJava { - dependsOn(smithyBuild) - } -} - // Helps Intellij IDE's discover smithy models sourceSets { main { diff --git a/examples/basic-server/settings.gradle.kts b/examples/basic-server/settings.gradle.kts index c608c023c5..9b3fe7b54b 100644 --- a/examples/basic-server/settings.gradle.kts +++ b/examples/basic-server/settings.gradle.kts @@ -4,9 +4,11 @@ pluginManagement { val smithyGradleVersion: String by settings + val smithyJavaVersion: String by settings plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) } repositories { diff --git a/examples/end-to-end/build.gradle.kts b/examples/end-to-end/build.gradle.kts index d1f5ad9225..e18dd65e2c 100644 --- a/examples/end-to-end/build.gradle.kts +++ b/examples/end-to-end/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - `java-library` - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") application } @@ -11,10 +10,6 @@ application { dependencies { val smithyJavaVersion: String by project - smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") - smithyBuild("software.amazon.smithy.java:client-core:$smithyJavaVersion") - smithyBuild("software.amazon.smithy.java:server-api:$smithyJavaVersion") - // Server dependencies implementation("software.amazon.smithy.java:server-netty:$smithyJavaVersion") implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") @@ -28,31 +23,14 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -// Add generated Java sources to the main sourceset -afterEvaluate { - val codegenPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$codegenPath/java") - } - resources { - srcDir("$codegenPath/resources") - } - } - create("it") { - compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] - runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output - } +sourceSets { + create("it") { + compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] + runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output } } tasks { - val smithyBuild by getting - compileJava { - dependsOn(smithyBuild) - } - val integ by registering(Test::class) { useJUnitPlatform() testClassesDirs = sourceSets["it"].output.classesDirs @@ -60,15 +38,6 @@ tasks { } } - -tasks.compileJava { - dependsOn(tasks.smithyBuild) -} - -tasks.processResources { - dependsOn(tasks.compileJava) -} - repositories { mavenLocal() mavenCentral() diff --git a/examples/end-to-end/gradle.properties b/examples/end-to-end/gradle.properties new file mode 100644 index 0000000000..0ce4688866 --- /dev/null +++ b/examples/end-to-end/gradle.properties @@ -0,0 +1,2 @@ +smithyJavaVersion=+ +smithyGradleVersion=1.4.0 diff --git a/examples/end-to-end/settings.gradle.kts b/examples/end-to-end/settings.gradle.kts index 32b9cb5629..1707eb5c3e 100644 --- a/examples/end-to-end/settings.gradle.kts +++ b/examples/end-to-end/settings.gradle.kts @@ -4,9 +4,11 @@ pluginManagement { val smithyGradleVersion: String by settings + val smithyJavaVersion: String by settings plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) } repositories { diff --git a/examples/event-streaming-client/build.gradle.kts b/examples/event-streaming-client/build.gradle.kts index dd91fa22d0..c792552c5a 100644 --- a/examples/event-streaming-client/build.gradle.kts +++ b/examples/event-streaming-client/build.gradle.kts @@ -1,14 +1,11 @@ plugins { - `java-library` - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") id("smithy-java.jmh-conventions") } dependencies { val smithyJavaVersion: String by project - smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") - smithyBuild("software.amazon.smithy.java:client-core:$smithyJavaVersion") implementation("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion") implementation("software.amazon.smithy.java:client-core:$smithyJavaVersion") implementation("software.amazon.smithy.java:client-rpcv2-cbor:${smithyJavaVersion}") @@ -19,27 +16,14 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -// Add generated Java sources to the main sourceset -afterEvaluate { - val clientPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$clientPath/java") - } - } - create("it") { - compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] - runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output - } +sourceSets { + create("it") { + compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] + runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output } } tasks { - val smithyBuild by getting - compileJava { - dependsOn(smithyBuild) - } val integ by registering(Test::class) { useJUnitPlatform() testClassesDirs = sourceSets["it"].output.classesDirs diff --git a/examples/event-streaming-client/settings.gradle.kts b/examples/event-streaming-client/settings.gradle.kts index 9fa6418909..b9989a5044 100644 --- a/examples/event-streaming-client/settings.gradle.kts +++ b/examples/event-streaming-client/settings.gradle.kts @@ -4,9 +4,11 @@ pluginManagement { val smithyGradleVersion: String by settings + val smithyJavaVersion: String by settings plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) } repositories { diff --git a/examples/lambda/build.gradle.kts b/examples/lambda/build.gradle.kts index 14cb7b625b..5b58655a43 100644 --- a/examples/lambda/build.gradle.kts +++ b/examples/lambda/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - `java-library` - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") } dependencies { @@ -9,9 +8,6 @@ dependencies { annotationProcessor("com.google.auto.service:auto-service:1.1.1") compileOnly("com.google.auto.service:auto-service:1.1.1") - smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") - smithyBuild("software.amazon.smithy.java:server-api:$smithyJavaVersion") - implementation("software.amazon.smithy.java:aws-lambda-endpoint:$smithyJavaVersion") implementation("software.amazon.smithy.java:server-api:$smithyJavaVersion") implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") @@ -35,24 +31,6 @@ tasks { } } -// Add generated Java files to the main sourceSet -afterEvaluate { - val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$serverPath/java") - } - } - } -} - -tasks { - compileJava { - dependsOn(smithyBuild) - } -} - // Helps Intellij IDE's discover smithy models sourceSets { main { diff --git a/examples/lambda/settings.gradle.kts b/examples/lambda/settings.gradle.kts index 6762903c55..4732f28979 100644 --- a/examples/lambda/settings.gradle.kts +++ b/examples/lambda/settings.gradle.kts @@ -4,9 +4,11 @@ pluginManagement { val smithyGradleVersion: String by settings + val smithyJavaVersion: String by settings plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) } repositories { diff --git a/examples/mcp-server/build.gradle.kts b/examples/mcp-server/build.gradle.kts index 2c93cba474..b232062008 100644 --- a/examples/mcp-server/build.gradle.kts +++ b/examples/mcp-server/build.gradle.kts @@ -1,16 +1,13 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer plugins { - `java-library` - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") id("com.gradleup.shadow") } dependencies { val smithyJavaVersion: String by project - smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") - smithyBuild("software.amazon.smithy.java:server-api:$smithyJavaVersion") implementation("software.amazon.smithy.java:smithy-ai-traits:$smithyJavaVersion") implementation("software.amazon.smithy.java:mcp-server:$smithyJavaVersion") implementation("software.amazon.smithy.java:server-proxy:$smithyJavaVersion") @@ -21,24 +18,6 @@ dependencies { implementation("software.amazon.smithy.java:aws-service-bundle:$smithyJavaVersion") } -// Add generated Java files to the main sourceSet -afterEvaluate { - val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$serverPath/java") - } - } - } -} - -tasks { - compileJava { - dependsOn(smithyBuild) - } -} - repositories { mavenLocal() mavenCentral() diff --git a/examples/mcp-server/settings.gradle.kts b/examples/mcp-server/settings.gradle.kts index fa821a2ce7..693519f409 100644 --- a/examples/mcp-server/settings.gradle.kts +++ b/examples/mcp-server/settings.gradle.kts @@ -4,9 +4,11 @@ pluginManagement { val smithyGradleVersion: String by settings + val smithyJavaVersion: String by settings plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) id("com.gradleup.shadow").version("8.3.5") } diff --git a/examples/restjson-client/build.gradle.kts b/examples/restjson-client/build.gradle.kts index 4e1727ab2c..6c02f1b952 100644 --- a/examples/restjson-client/build.gradle.kts +++ b/examples/restjson-client/build.gradle.kts @@ -1,15 +1,12 @@ plugins { - `java-library` + id("software.amazon.smithy.java.gradle.smithy-java") application - id("software.amazon.smithy.gradle.smithy-base") id("smithy-java.jmh-conventions") } dependencies { val smithyJavaVersion: String by project - smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") - smithyBuild("software.amazon.smithy.java:client-core:$smithyJavaVersion") implementation("software.amazon.smithy.java:client-core:$smithyJavaVersion") api("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion") @@ -27,28 +24,14 @@ application { mainClass = "software.amazon.smithy.java.example.ClientExample" } -// Add generated Java sources to the main sourceset -afterEvaluate { - val clientPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$clientPath/java") - } - } - create("it") { - compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] - runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output - } +sourceSets { + create("it") { + compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] + runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output } } tasks { - val smithyBuild by getting - compileJava { - dependsOn(smithyBuild) - } - val integ by registering(Test::class) { useJUnitPlatform() testClassesDirs = sourceSets["it"].output.classesDirs diff --git a/examples/restjson-client/settings.gradle.kts b/examples/restjson-client/settings.gradle.kts index 5b2499179d..9a4e393a40 100644 --- a/examples/restjson-client/settings.gradle.kts +++ b/examples/restjson-client/settings.gradle.kts @@ -4,9 +4,11 @@ pluginManagement { val smithyGradleVersion: String by settings + val smithyJavaVersion: String by settings plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) } repositories { diff --git a/examples/transcribestreaming-client/build.gradle.kts b/examples/transcribestreaming-client/build.gradle.kts index ab001a1e4e..21827e18a8 100644 --- a/examples/transcribestreaming-client/build.gradle.kts +++ b/examples/transcribestreaming-client/build.gradle.kts @@ -1,14 +1,10 @@ plugins { - `java-library` - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") } dependencies { val smithyJavaVersion: String by project - smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") - smithyBuild("software.amazon.smithy.java:client-core:$smithyJavaVersion") - implementation("software.amazon.api.models:transcribe-streaming:1.0.8") implementation("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion") implementation("software.amazon.smithy.java:client-core:$smithyJavaVersion") @@ -26,33 +22,14 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -// Add generated Java sources to the main sourceset -afterEvaluate { - val clientPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() - sourceSets { - main { - java { - srcDir("$clientPath/java") - } - resources { - srcDir("$clientPath/resources") - } - } - create("it") { - compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] - runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output - } +sourceSets { + create("it") { + compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] + runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output } } tasks { - val smithyBuild by getting - compileJava { - dependsOn(smithyBuild) - } - processResources { - dependsOn(smithyBuild) - } val integ by registering(Test::class) { useJUnitPlatform() testClassesDirs = sourceSets["it"].output.classesDirs @@ -60,7 +37,6 @@ tasks { } } - repositories { mavenLocal() mavenCentral() diff --git a/examples/transcribestreaming-client/settings.gradle.kts b/examples/transcribestreaming-client/settings.gradle.kts index 459da732b8..2c5407969d 100644 --- a/examples/transcribestreaming-client/settings.gradle.kts +++ b/examples/transcribestreaming-client/settings.gradle.kts @@ -4,9 +4,11 @@ pluginManagement { val smithyGradleVersion: String by settings + val smithyJavaVersion: String by settings plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) } repositories { From 37fc435b5a8d63221750f6c8cefb518b46165560 Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Thu, 11 Jun 2026 14:31:14 -0700 Subject: [PATCH 4/6] Don't auto add Java plugins, instead react to whatever plugin is applied and decided api vs implementation based on modes --- README.md | 1 + gradle-plugin/README.md | 26 ++++-- .../java/gradle/SmithyJavaExtension.java | 10 ++- .../smithy/java/gradle/SmithyJavaPlugin.java | 85 +++++++++++++------ 4 files changed, 88 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index dd452dceb5..950f3bed6a 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ source set wiring, and task ordering automatically: ```kotlin // build.gradle.kts plugins { + `java-library` // or `java` / `application` for leaf projects id("software.amazon.smithy.java.gradle.smithy-java") version "" } ``` diff --git a/gradle-plugin/README.md b/gradle-plugin/README.md index e76fbb3260..f42a6ae28c 100644 --- a/gradle-plugin/README.md +++ b/gradle-plugin/README.md @@ -38,12 +38,13 @@ tasks.processResources { dependsOn("smithyBuild") } **After** (with this plugin): ```kotlin plugins { + `java-library` id("software.amazon.smithy.java.gradle.smithy-java") version "" } ``` -That's it. The plugin handles `java-library`, `smithy-base`, dependency management, source set -wiring, and task ordering automatically. +That's it. The plugin handles `smithy-base`, dependency management, source set wiring, and task +ordering automatically. ## Installation @@ -61,10 +62,11 @@ pluginManagement { } ``` -Then apply it in your `build.gradle.kts`: +Then apply it in your `build.gradle.kts` along with a Java plugin: ```kotlin plugins { + `java-library` // or `java` / `application` for leaf projects id("software.amazon.smithy.java.gradle.smithy-java") } ``` @@ -92,6 +94,7 @@ The simplest case, generate data types from a Smithy model with no client or ser `build.gradle.kts`: ```kotlin plugins { + `java-library` id("software.amazon.smithy.java.gradle.smithy-java") } ``` @@ -120,12 +123,13 @@ Generate a client for a service. `build.gradle.kts`: ```kotlin plugins { + `java-library` id("software.amazon.smithy.java.gradle.smithy-java") } dependencies { // Add the protocol and transport your service uses - api("software.amazon.smithy.java:aws-client-restjson:") + implementation("software.amazon.smithy.java:aws-client-restjson:") } ``` @@ -154,6 +158,7 @@ Generate server stubs. `build.gradle.kts`: ```kotlin plugins { + java id("software.amazon.smithy.java.gradle.smithy-java") } @@ -209,8 +214,16 @@ Service files from multiple plugins are merged automatically. Disable with ### `autoAddDependencies` When `true` (default), the plugin automatically adds the correct Smithy Java runtime -dependencies based on the active modes. Set to `false` when you need full control over -dependency versions or want to use project dependencies (e.g. in a monorepo): +dependencies based on the active modes. The configuration used depends on which Java plugin +you apply and the codegen mode: + +- **`java-library`**: types/client dependencies (`core`, `framework-errors`, `client-core`) + are added to `api`, making them transitively available to consumers. Server dependencies + (`server-api`) are added to `implementation` since servers are typically not re-exported. +- **`java` / `application`**: all runtime dependencies are added to `implementation`. + +Set to `false` when you need full control over dependency versions or want to use project +dependencies (e.g. in a monorepo): ```kotlin smithyJava { @@ -228,6 +241,7 @@ dependencies { - Java 21 or later - Gradle 8.5 or later (first version to support Java 21 runtime) +- A Java plugin applied (`java`, `java-library`, or `application`) - A `smithy-build.json` with a `java-codegen` plugin configured ## Custom Source Projection diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java index ae657056f2..44d8a845f0 100644 --- a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java @@ -38,11 +38,15 @@ public SmithyJavaExtension() { * *

    *
  • Always: {@code codegen-plugin} to smithyBuild; {@code core} and - * {@code framework-errors} to api
  • - *
  • Client mode: {@code client-core} to smithyBuild and api
  • - *
  • Server mode: {@code server-api} to smithyBuild and api
  • + * {@code framework-errors} to api/implementation + *
  • Client mode: {@code client-core} to api/implementation
  • + *
  • Server mode: {@code server-api} to implementation
  • *
* + *

When {@code java-library} is applied, types/client dependencies use {@code api} + * and server dependencies use {@code implementation}. When only {@code java} or + * {@code application} is applied, all dependencies use {@code implementation}. + * *

Set to {@code false} to manage all dependencies manually. * * @return property controlling auto-dependency management diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java index 1ea1226d91..519ba4ac9d 100644 --- a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java @@ -12,6 +12,7 @@ import java.util.concurrent.Callable; import java.util.stream.Collectors; +import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; @@ -19,6 +20,7 @@ import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.file.FileCollection; import org.gradle.api.plugins.JavaLibraryPlugin; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Delete; import org.gradle.api.tasks.SourceSet; @@ -32,8 +34,9 @@ /** * Gradle plugin that simplifies Java code generation from Smithy models. * - *

This plugin applies {@code java-library} and - * {@code software.amazon.smithy.gradle.smithy-base}, then automatically: + *

This plugin requires a Java plugin ({@code java}, {@code java-library}, or {@code application}) + * to be applied by the user, then applies {@code software.amazon.smithy.gradle.smithy-base} and + * automatically: *

    *
  • Parses {@code smithy-build.json} to determine codegen modes
  • *
  • Adds required dependencies based on detected modes
  • @@ -41,6 +44,14 @@ *
  • Sets up task dependencies (compileJava, processResources, sourcesJar)
  • *
  • Optionally merges META-INF/services files from multiple plugin outputs
  • *
+ * + *

When {@code java-library} is applied, types/client dependencies are added to the {@code api} + * configuration so they are transitively visible to consumers, while server dependencies use + * {@code implementation}. When only {@code java} or {@code application} is applied, all + * dependencies are added to {@code implementation}. + * + *

Users who need full control over dependency configurations can set + * {@code smithyJava.autoAddDependencies = false} and manage dependencies manually. */ public class SmithyJavaPlugin implements Plugin { @@ -52,20 +63,28 @@ public class SmithyJavaPlugin implements Plugin { @Override public void apply(Project project) { - project.getPlugins().apply(JavaLibraryPlugin.class); project.getPlugins().apply("software.amazon.smithy.gradle.smithy-base"); SmithyJavaExtension ext = project.getExtensions() .create("smithyJava", SmithyJavaExtension.class); - SmithyExtension smithyExt = project.getExtensions() - .getByType(SmithyExtension.class); + project.getPlugins().withType(JavaPlugin.class, javaPlugin -> { + SmithyExtension smithyExt = project.getExtensions() + .getByType(SmithyExtension.class); - configureDependencies(project, smithyExt, ext); - wireGeneratedSources(project, smithyExt, ext); - configureCleanOutput(project, smithyExt); - configureTaskDependencies(project); - configureServiceFileMerging(project, smithyExt, ext); + configureDependencies(project, smithyExt, ext); + wireGeneratedSources(project, smithyExt, ext); + configureCleanOutput(project, smithyExt); + configureTaskDependencies(project); + configureServiceFileMerging(project, smithyExt, ext); + }); + + project.afterEvaluate(p -> { + if (!p.getPlugins().hasPlugin(JavaPlugin.class)) { + throw new GradleException( + "The smithy-java plugin requires a Java plugin (java, java-library, or application) to be applied."); + } + }); } private void configureDependencies( @@ -74,7 +93,6 @@ private void configureDependencies( SmithyJavaExtension ext ) { Configuration smithyBuild = project.getConfigurations().getByName("smithyBuild"); - Configuration api = project.getConfigurations().getByName("api"); // Resolve modes via ValueSource for configuration cache compatibility. // If explicit modes are set in the DSL, use those directly; otherwise @@ -91,32 +109,49 @@ private void configureDependencies( if (!ext.getAutoAddDependencies().getOrElse(true)) { return; } - String version = SmithyJavaVersion.VERSION; - Set resolved = modes.get(); - addIfAbsent(deps, project.getDependencies(), "codegen-plugin", version); - if (resolved.contains("client")) { - addIfAbsent(deps, project.getDependencies(), "client-core", version); - } - if (resolved.contains("server")) { - addIfAbsent(deps, project.getDependencies(), "server-api", version); - } + addIfAbsent(deps, project.getDependencies(), "codegen-plugin", SmithyJavaVersion.VERSION); }); - api.withDependencies(deps -> { + // Wire runtime deps to implementation by default. The withDependencies callback + // checks at resolution time whether java-library has been applied (api exists) + // and skips deps that belong on api instead. + Configuration implementation = project.getConfigurations().getByName("implementation"); + implementation.withDependencies(deps -> { if (!ext.getAutoAddDependencies().getOrElse(true)) { return; } + boolean hasApi = project.getPlugins().hasPlugin(JavaLibraryPlugin.class); String version = SmithyJavaVersion.VERSION; Set resolved = modes.get(); - addIfAbsent(deps, project.getDependencies(), "core", version); - addIfAbsent(deps, project.getDependencies(), "framework-errors", version); - if (resolved.contains("client")) { - addIfAbsent(deps, project.getDependencies(), "client-core", version); + if (!hasApi) { + addIfAbsent(deps, project.getDependencies(), "core", version); + addIfAbsent(deps, project.getDependencies(), "framework-errors", version); + if (resolved.contains("client")) { + addIfAbsent(deps, project.getDependencies(), "client-core", version); + } } if (resolved.contains("server")) { addIfAbsent(deps, project.getDependencies(), "server-api", version); } }); + + // When java-library is applied, types/client deps go to api. + // Use withType to react regardless of plugin application order. + project.getPlugins().withType(JavaLibraryPlugin.class, plugin -> { + Configuration api = project.getConfigurations().getByName("api"); + api.withDependencies(deps -> { + if (!ext.getAutoAddDependencies().getOrElse(true)) { + return; + } + String version = SmithyJavaVersion.VERSION; + Set resolved = modes.get(); + addIfAbsent(deps, project.getDependencies(), "core", version); + addIfAbsent(deps, project.getDependencies(), "framework-errors", version); + if (resolved.contains("client")) { + addIfAbsent(deps, project.getDependencies(), "client-core", version); + } + }); + }); } private void wireGeneratedSources( From 4337aeb2b8c673f3853101a32ee6cc776788bd76 Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Thu, 11 Jun 2026 14:36:51 -0700 Subject: [PATCH 5/6] Make examples actually standalone --- examples/basic-server/gradle.properties | 2 ++ examples/event-streaming-client/build.gradle.kts | 8 ++------ examples/event-streaming-client/gradle.properties | 2 ++ examples/lambda/build.gradle.kts | 1 + examples/lambda/gradle.properties | 2 ++ examples/mcp-server/build.gradle.kts | 1 + examples/mcp-server/gradle.properties | 2 ++ examples/restjson-client/build.gradle.kts | 5 +++-- examples/restjson-client/gradle.properties | 2 ++ examples/restjson-client/settings.gradle.kts | 1 + examples/standalone-types/build.gradle.kts | 1 + examples/transcribestreaming-client/build.gradle.kts | 10 ++++++---- examples/transcribestreaming-client/gradle.properties | 2 ++ 13 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 examples/basic-server/gradle.properties create mode 100644 examples/event-streaming-client/gradle.properties create mode 100644 examples/lambda/gradle.properties create mode 100644 examples/mcp-server/gradle.properties create mode 100644 examples/restjson-client/gradle.properties create mode 100644 examples/transcribestreaming-client/gradle.properties diff --git a/examples/basic-server/gradle.properties b/examples/basic-server/gradle.properties new file mode 100644 index 0000000000..0ce4688866 --- /dev/null +++ b/examples/basic-server/gradle.properties @@ -0,0 +1,2 @@ +smithyJavaVersion=+ +smithyGradleVersion=1.4.0 diff --git a/examples/event-streaming-client/build.gradle.kts b/examples/event-streaming-client/build.gradle.kts index c792552c5a..8e2457084f 100644 --- a/examples/event-streaming-client/build.gradle.kts +++ b/examples/event-streaming-client/build.gradle.kts @@ -1,6 +1,6 @@ plugins { + `java-library` id("software.amazon.smithy.java.gradle.smithy-java") - id("smithy-java.jmh-conventions") } dependencies { @@ -9,7 +9,7 @@ dependencies { implementation("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion") implementation("software.amazon.smithy.java:client-core:$smithyJavaVersion") implementation("software.amazon.smithy.java:client-rpcv2-cbor:${smithyJavaVersion}") - implementation(project(":framework-errors")) + implementation("software.amazon.smithy.java:framework-errors:$smithyJavaVersion") // Test dependencies testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") @@ -31,10 +31,6 @@ tasks { } } -jmh { - warmupIterations = 2 -} - // Helps Intellij IDE's discover smithy models sourceSets { main { diff --git a/examples/event-streaming-client/gradle.properties b/examples/event-streaming-client/gradle.properties new file mode 100644 index 0000000000..0ce4688866 --- /dev/null +++ b/examples/event-streaming-client/gradle.properties @@ -0,0 +1,2 @@ +smithyJavaVersion=+ +smithyGradleVersion=1.4.0 diff --git a/examples/lambda/build.gradle.kts b/examples/lambda/build.gradle.kts index 5b58655a43..7afab16662 100644 --- a/examples/lambda/build.gradle.kts +++ b/examples/lambda/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + java id("software.amazon.smithy.java.gradle.smithy-java") } diff --git a/examples/lambda/gradle.properties b/examples/lambda/gradle.properties new file mode 100644 index 0000000000..0ce4688866 --- /dev/null +++ b/examples/lambda/gradle.properties @@ -0,0 +1,2 @@ +smithyJavaVersion=+ +smithyGradleVersion=1.4.0 diff --git a/examples/mcp-server/build.gradle.kts b/examples/mcp-server/build.gradle.kts index b232062008..51337fa4de 100644 --- a/examples/mcp-server/build.gradle.kts +++ b/examples/mcp-server/build.gradle.kts @@ -1,6 +1,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer plugins { + java id("software.amazon.smithy.java.gradle.smithy-java") id("com.gradleup.shadow") } diff --git a/examples/mcp-server/gradle.properties b/examples/mcp-server/gradle.properties new file mode 100644 index 0000000000..0ce4688866 --- /dev/null +++ b/examples/mcp-server/gradle.properties @@ -0,0 +1,2 @@ +smithyJavaVersion=+ +smithyGradleVersion=1.4.0 diff --git a/examples/restjson-client/build.gradle.kts b/examples/restjson-client/build.gradle.kts index 6c02f1b952..2aeb5a1f06 100644 --- a/examples/restjson-client/build.gradle.kts +++ b/examples/restjson-client/build.gradle.kts @@ -1,7 +1,8 @@ plugins { + `java-library` id("software.amazon.smithy.java.gradle.smithy-java") application - id("smithy-java.jmh-conventions") + id("me.champeau.jmh") } dependencies { @@ -17,7 +18,7 @@ dependencies { // Test dependencies testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation(libs.assertj.core) + testImplementation("org.assertj:assertj-core:3.27.7") } application { diff --git a/examples/restjson-client/gradle.properties b/examples/restjson-client/gradle.properties new file mode 100644 index 0000000000..0ce4688866 --- /dev/null +++ b/examples/restjson-client/gradle.properties @@ -0,0 +1,2 @@ +smithyJavaVersion=+ +smithyGradleVersion=1.4.0 diff --git a/examples/restjson-client/settings.gradle.kts b/examples/restjson-client/settings.gradle.kts index 9a4e393a40..090c6a13d4 100644 --- a/examples/restjson-client/settings.gradle.kts +++ b/examples/restjson-client/settings.gradle.kts @@ -9,6 +9,7 @@ pluginManagement { plugins { id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) id("software.amazon.smithy.java.gradle.smithy-java").version(smithyJavaVersion) + id("me.champeau.jmh").version("0.7.3") } repositories { diff --git a/examples/standalone-types/build.gradle.kts b/examples/standalone-types/build.gradle.kts index 5d7adab3f9..1780973b29 100644 --- a/examples/standalone-types/build.gradle.kts +++ b/examples/standalone-types/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + `java-library` id("software.amazon.smithy.java.gradle.smithy-java") } diff --git a/examples/transcribestreaming-client/build.gradle.kts b/examples/transcribestreaming-client/build.gradle.kts index 21827e18a8..9b57c61d56 100644 --- a/examples/transcribestreaming-client/build.gradle.kts +++ b/examples/transcribestreaming-client/build.gradle.kts @@ -1,9 +1,11 @@ plugins { + `java-library` id("software.amazon.smithy.java.gradle.smithy-java") } dependencies { val smithyJavaVersion: String by project + val smithyVersion = "1.71.0" implementation("software.amazon.api.models:transcribe-streaming:1.0.8") implementation("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion") @@ -12,12 +14,12 @@ dependencies { implementation("software.amazon.smithy.java:client-rulesengine:$smithyJavaVersion") implementation("software.amazon.smithy.java:aws-client-rulesengine:$smithyJavaVersion") implementation("org.slf4j:slf4j-simple:2.0.18") - implementation(libs.smithy.aws.endpoints) - implementation(libs.smithy.aws.smoke.test.model) - implementation(libs.smithy.aws.traits) + implementation("software.amazon.smithy:smithy-aws-endpoints:$smithyVersion") + implementation("software.amazon.smithy:smithy-aws-smoke-test-model:$smithyVersion") + implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion") // Test dependencies - testImplementation(project(":aws:sdkv2:aws-sdkv2-auth")) + testImplementation("software.amazon.smithy.java:aws-sdkv2-auth:$smithyJavaVersion") testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/examples/transcribestreaming-client/gradle.properties b/examples/transcribestreaming-client/gradle.properties new file mode 100644 index 0000000000..0ce4688866 --- /dev/null +++ b/examples/transcribestreaming-client/gradle.properties @@ -0,0 +1,2 @@ +smithyJavaVersion=+ +smithyGradleVersion=1.4.0 From e556773584629274396558687bd1fdfa2cd45eba Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Thu, 11 Jun 2026 14:49:09 -0700 Subject: [PATCH 6/6] Avoid smithyBuild not being up-to-date incorrectly and use plugin everywhere --- benchmarks/serde-benchmarks/build.gradle.kts | 93 ++--------- build.gradle.kts | 8 + examples/build.gradle.kts | 9 -- examples/end-to-end/build.gradle.kts | 1 - gradle-plugin/README.md | 20 ++- .../gradle/SmithyBuildModesValueSource.java | 23 ++- .../java/gradle/SmithyJavaExtension.java | 39 ++++- .../smithy/java/gradle/SmithyJavaPlugin.java | 147 +++++++++++------- .../gradle/tasks/MergeServiceFilesTask.java | 9 +- mcp/mcp-schemas/build.gradle.kts | 6 - 10 files changed, 190 insertions(+), 165 deletions(-) diff --git a/benchmarks/serde-benchmarks/build.gradle.kts b/benchmarks/serde-benchmarks/build.gradle.kts index 3c378ff7d3..6b5c382829 100644 --- a/benchmarks/serde-benchmarks/build.gradle.kts +++ b/benchmarks/serde-benchmarks/build.gradle.kts @@ -2,17 +2,22 @@ plugins { id("smithy-java.java-conventions") id("com.gradleup.shadow") id("smithy-java.jmh-conventions") - id("software.amazon.smithy.gradle.smithy-base") + id("software.amazon.smithy.java.gradle.smithy-java") } description = "Serde (serialization/deserialization) microbenchmarks for smithy-java codecs." -// Not published. No `smithy-java.module-conventions`, no publishing, no BOM entry. +smithyJava { + projections.addAll( + "aws-json-rpc-1-0-client", + "aws-query-client", + "rest-json-client", + "rest-xml-client", + "rpc-v2-cbor-client", + ) +} // Benchmarks intentionally target JDK 25 (the rest of smithy-java targets 21). -// Performance measurements should reflect the latest JIT/runtime improvements -// available to consumers; the runtime deps were compiled for 21 but run fine -// on a newer JVM. java { toolchain { languageVersion = JavaLanguageVersion.of(25) @@ -24,33 +29,18 @@ tasks.withType().configureEach { } dependencies { - // Smithy traits needed at build time (smithy-build) and at runtime (when - // BenchmarkTestCases loads the model). The jmh configuration extends - // implementation, so these are available to both. implementation(libs.smithy.model) implementation(libs.smithy.aws.traits) implementation(libs.smithy.protocol.traits) implementation(libs.smithy.protocol.test.traits) implementation(libs.smithy.utils) - // The Smithy Java codegen plugin produces typed shape classes plus - // ApiOperation classes per service (see `smithy-build.json`). The - // client-core dep is required because the generated client classes - // reference it. - smithyBuild(project(":codegen:codegen-plugin")) - smithyBuild(project(":client:client-core")) - - // smithy-java runtime stack — what we are benchmarking. - jmh(project(":core")) jmh(project(":io")) jmh(project(":logging")) jmh(project(":codecs:json-codec", configuration = "shadow")) jmh(project(":codecs:cbor-codec")) jmh(project(":codecs:xml-codec")) - // Client protocols — every benchmark drives the corresponding - // ClientProtocol#createRequest / #deserializeResponse, mirroring the - // reference implementation's protocol-level entry points. jmh(project(":client:client-core")) jmh(project(":client:client-http")) jmh(project(":client:client-http-binding")) @@ -60,16 +50,9 @@ dependencies { jmh(project(":aws:client:aws-client-restjson")) jmh(project(":aws:client:aws-client-restxml")) - // Protocol test document for converting test case params into typed shapes. jmh(project(":protocol-test-harness")) } -// Smithy benchmark model files (tagged @httpRequestTests / @httpResponseTests -// with the `serde-benchmark` tag). -// -// At runtime BenchmarkContext loads the model via -// `Model.assembler().discoverModels()`, which walks `META-INF/smithy/manifest` -// resources on the classpath. abstract class GenerateSmithyManifest : DefaultTask() { @get:InputDirectory abstract val sourceDir: DirectoryProperty @@ -77,7 +60,7 @@ abstract class GenerateSmithyManifest : DefaultTask() { @get:OutputDirectory abstract val outputDir: DirectoryProperty - @org.gradle.api.tasks.TaskAction + @TaskAction fun run() { val outRoot = outputDir.get().asFile val smithyDir = outRoot.resolve("META-INF/smithy") @@ -104,50 +87,14 @@ val generateSmithyManifest by tasks.registering(GenerateSmithyManifest::class) { outputDir.set(layout.buildDirectory.dir("generated-resources/smithy-manifest")) } -// Wire each codegen projection's output into the jmh source set. There are -// five projections (one per service); each emits Java source under -// `build/smithyprojections///java-codegen/java` into a -// distinct package so same-named typed shapes from different protocols don't -// collide. -val codegenProjections = - listOf( - "aws-json-rpc-1-0-client", - "aws-query-client", - "rest-json-client", - "rest-xml-client", - "rpc-v2-cbor-client", - ) - -afterEvaluate { - val projectionPaths = - codegenProjections.map { name -> - smithy.getPluginProjectionPath(name, "java-codegen").get() - } - sourceSets.named("jmh") { - java { - projectionPaths.forEach { srcDir("$it/java") } - } - resources { - projectionPaths.forEach { srcDir("$it/resources") } - srcDir(generateSmithyManifest) - } +sourceSets.named("jmh") { + resources { + srcDir(generateSmithyManifest) } } tasks.named("processJmhResources") { dependsOn(generateSmithyManifest) - dependsOn("smithyBuild") -} - -// Multiple codegen projections each register META-INF/services/...SchemaIndex -// (and other SPI files). Both should be on the runtime classpath; tell -// processJmhResources to keep both entries by concatenating. -tasks.named("processJmhResources") { - duplicatesStrategy = DuplicatesStrategy.INCLUDE -} - -tasks.named("compileJmhJava") { - dependsOn("smithyBuild") } // Test case: -Pjmh.testCaseId=rpcv2Cbor_PutItemRequest_BinaryData_S @@ -176,23 +123,11 @@ jmh { resultsFile = layout.buildDirectory.file("results/jmh/results.json") } -// With shadow applied before jmh, jmhJar is a ShadowJar. Configure -// mergeServiceFiles() so duplicate META-INF/services/ entries from -// multiple codegen projections are concatenated rather than overwritten. tasks.jmhJar { mergeServiceFiles() append("META-INF/smithy/manifest") } -// Run the cross-language result converter. Reads the JMH JSON written by the -// `jmh` task and writes a JSON + Markdown pair conforming to the shared -// benchmark output schema. OS, instance type (via EC2 IMDS), and smithy-java -// version are detected at runtime. -// -// ./gradlew :benchmarks:serde-benchmarks:convertJmhResults -// -// Optional properties: -// -PoutputPrefix= prefix for the output files (default: build/results/jmh/output) tasks.register("convertJmhResults") { group = "benchmarks" description = "Convert JMH JSON output to the cross-language serde benchmark schema." diff --git a/build.gradle.kts b/build.gradle.kts index 39262de851..b8d1c6a462 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,6 +25,14 @@ val smithyJavaVersion = project.file("VERSION").readText().replace(System.lineSe allprojects { group = "software.amazon.smithy.java" version = smithyJavaVersion + + configurations.all { + resolutionStrategy.dependencySubstitution { + rootProject.allprojects.forEach { + substitute(module("${it.group}:${it.name}")).using(project(it.path)) + } + } + } } println("Smithy-Java version: '${smithyJavaVersion}'") diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index bc4a9bd197..9b6dd2a123 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -1,16 +1,7 @@ -// Substitute any maven module dependencies with and project dependencies subprojects { group = "software.amazon.smithy.java.example" - configurations.all { - resolutionStrategy.dependencySubstitution { - rootProject.allprojects.forEach { - substitute(module("${it.group}:${it.name}")).using(project(it.path)) - } - } - } - plugins.withId("java") { the().toolchain { languageVersion.set(JavaLanguageVersion.of(21)) diff --git a/examples/end-to-end/build.gradle.kts b/examples/end-to-end/build.gradle.kts index e18dd65e2c..d1b201c38e 100644 --- a/examples/end-to-end/build.gradle.kts +++ b/examples/end-to-end/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { // Client dependencies implementation("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion") - implementation("software.amazon.smithy.java:client-core:$smithyJavaVersion") // Test dependencies testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") diff --git a/gradle-plugin/README.md b/gradle-plugin/README.md index f42a6ae28c..167dfc3326 100644 --- a/gradle-plugin/README.md +++ b/gradle-plugin/README.md @@ -197,6 +197,21 @@ When set, these override whatever is in `smithy-build.json` for dependency resol When empty (default), modes are read from `smithy-build.json` automatically. +### `projections` + +When your `smithy-build.json` uses named projections (instead of or in addition to the +default source projection), list them here so the plugin wires each projection's +`java-codegen` output into the main source set: + +```kotlin +smithyJava { + projections.addAll("rest-json-client", "rpc-v2-cbor-client") +} +``` + +When empty (default), the plugin uses the source projection. Service files from multiple +projections are merged automatically. + ### `generatedPluginOutputs` When your `smithy-build.json` has plugins beyond `java-codegen` that produce Java source @@ -217,9 +232,8 @@ When `true` (default), the plugin automatically adds the correct Smithy Java run dependencies based on the active modes. The configuration used depends on which Java plugin you apply and the codegen mode: -- **`java-library`**: types/client dependencies (`core`, `framework-errors`, `client-core`) - are added to `api`, making them transitively available to consumers. Server dependencies - (`server-api`) are added to `implementation` since servers are typically not re-exported. +- **`java-library`**: all runtime dependencies are added to `api` unless the mode is + server-only (no types or client), in which case `implementation` is used. - **`java` / `application`**: all runtime dependencies are added to `implementation`. Set to `false` when you need full control over dependency versions or want to use project diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java index 3228b76bbc..e95513186f 100644 --- a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.nio.file.Files; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import org.gradle.api.provider.SetProperty; import org.gradle.api.provider.ValueSource; @@ -35,6 +36,7 @@ public interface Params extends ValueSourceParameters { @Override public Set obtain() { + Set allModes = new HashSet<>(); for (File config : getParameters().getSmithyBuildConfigs().get()) { if (!config.exists()) { continue; @@ -42,21 +44,34 @@ public Set obtain() { try { String content = Files.readString(config.toPath()); ObjectNode root = Node.parseJsonWithComments(content).expectObjectNode(); - return root.getObjectMember("plugins") + + root.getObjectMember("plugins") .flatMap(plugins -> plugins.getObjectMember(JAVA_CODEGEN_PLUGIN_NAME)) .flatMap(codegen -> codegen.getArrayMember("modes")) .map(SmithyBuildModesValueSource::extractModes) - .orElse(Set.of("types")); + .ifPresent(allModes::addAll); + root.getObjectMember("projections").ifPresent(projections -> { + for (Node value : projections.getMembers().values()) { + value.expectObjectNode() + .getObjectMember("plugins") + .flatMap(plugins -> plugins.getObjectMember(JAVA_CODEGEN_PLUGIN_NAME)) + .flatMap(codegen -> codegen.getArrayMember("modes")) + .map(SmithyBuildModesValueSource::extractModes) + .ifPresent(allModes::addAll); + } + }); } catch (IOException ignored) { } } - return Set.of("types"); + return allModes.isEmpty() ? Set.of("types") : allModes; } private static Set extractModes(ArrayNode modesArray) { Set modes = new HashSet<>(); for (Node element : modesArray.getElements()) { - element.asStringNode().map(StringNode::getValue).ifPresent(modes::add); + element.asStringNode() + .map(s -> s.getValue().toLowerCase(Locale.ENGLISH)) + .ifPresent(modes::add); } return modes; } diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java index 44d8a845f0..3c00dbeb1f 100644 --- a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java @@ -28,6 +28,7 @@ public abstract class SmithyJavaExtension { public SmithyJavaExtension() { getAutoAddDependencies().convention(true); getModes().convention(Collections.emptySet()); + getProjections().convention(Collections.emptyList()); getGeneratedPluginOutputs().convention(Collections.emptyList()); getMergeServiceFiles().convention(true); } @@ -39,13 +40,14 @@ public SmithyJavaExtension() { *

    *
  • Always: {@code codegen-plugin} to smithyBuild; {@code core} and * {@code framework-errors} to api/implementation
  • - *
  • Client mode: {@code client-core} to api/implementation
  • - *
  • Server mode: {@code server-api} to implementation
  • + *
  • Client mode: {@code client-core} to smithyBuild and api/implementation
  • + *
  • Server mode: {@code server-api} to smithyBuild and api/implementation
  • *
* - *

When {@code java-library} is applied, types/client dependencies use {@code api} - * and server dependencies use {@code implementation}. When only {@code java} or - * {@code application} is applied, all dependencies use {@code implementation}. + *

When {@code java-library} is applied, all runtime dependencies use {@code api} + * unless the mode is server-only, in which case {@code implementation} is used. + * When only {@code java} or {@code application} is applied, all dependencies use + * {@code implementation}. * *

Set to {@code false} to manage all dependencies manually. * @@ -73,6 +75,26 @@ public SmithyJavaExtension() { */ public abstract SetProperty getModes(); + /** + * Explicit list of projection names whose {@code java-codegen} output should be + * wired into the main source set. When non-empty, these are used instead of + * the default source projection. + * + *

This is useful for multi-projection builds where each projection generates + * code for a different service or protocol: + *

{@code
+     * smithyJava {
+     *     projections.addAll("rest-json-client", "rpc-v2-cbor-client")
+     * }
+     * }
+ * + *

When empty (default), the plugin uses the single source projection from + * the {@code smithy} extension. + * + * @return list of projection names to wire + */ + public abstract ListProperty getProjections(); + /** * Additional Smithy build plugin names (as declared in {@code smithy-build.json}) * whose generated output directories should be wired into the Java source set. @@ -87,10 +109,11 @@ public SmithyJavaExtension() { public abstract ListProperty getGeneratedPluginOutputs(); /** - * Whether to automatically merge {@code META-INF/services} files when - * {@link #getGeneratedPluginOutputs()} is non-empty. Defaults to {@code true}. + * Whether to automatically merge {@code META-INF/services} files when multiple + * projections or {@link #getGeneratedPluginOutputs()} are configured. Defaults to + * {@code true}. * - *

When multiple Smithy build plugins produce service provider files, they + *

When multiple projections or plugins produce service provider files, they * may conflict. This option enables a merge task that combines them. * * @return property controlling service file merging diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java index 519ba4ac9d..8e4d1bede6 100644 --- a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaPlugin.java @@ -6,12 +6,13 @@ package software.amazon.smithy.java.gradle; import java.io.File; -import java.nio.file.Path; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.stream.Collectors; +import javax.inject.Inject; + import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -19,6 +20,7 @@ import org.gradle.api.artifacts.DependencySet; import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileSystemOperations; import org.gradle.api.plugins.JavaLibraryPlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.provider.Provider; @@ -34,26 +36,29 @@ /** * Gradle plugin that simplifies Java code generation from Smithy models. * - *

This plugin requires a Java plugin ({@code java}, {@code java-library}, or {@code application}) - * to be applied by the user, then applies {@code software.amazon.smithy.gradle.smithy-base} and + *

This plugin requires the user to apply a Java plugin ({@code java}, {@code java-library}, or + * {@code application}) and then applies {@code software.amazon.smithy.gradle.smithy-base}. It * automatically: *

    - *
  • Parses {@code smithy-build.json} to determine codegen modes
  • - *
  • Adds required dependencies based on detected modes
  • + *
  • Parses {@code smithy-build.json} to determine codegen modes (client, server, types)
  • + *
  • Adds required runtime and codegen dependencies based on detected modes
  • *
  • Wires generated source and resource directories into the main source set
  • *
  • Sets up task dependencies (compileJava, processResources, sourcesJar)
  • - *
  • Optionally merges META-INF/services files from multiple plugin outputs
  • + *
  • Merges META-INF/services files when multiple projections or plugin outputs are used
  • *
* - *

When {@code java-library} is applied, types/client dependencies are added to the {@code api} - * configuration so they are transitively visible to consumers, while server dependencies use - * {@code implementation}. When only {@code java} or {@code application} is applied, all - * dependencies are added to {@code implementation}. + *

Dependency configuration selection: + *

    + *
  • {@code java-library}: uses {@code api} unless mode is server-only
  • + *
  • {@code java} or {@code application}: uses {@code implementation}
  • + *
* *

Users who need full control over dependency configurations can set - * {@code smithyJava.autoAddDependencies = false} and manage dependencies manually. + * {@code smithyJava.autoAddDependencies = false}. + * + * @see SmithyJavaExtension */ -public class SmithyJavaPlugin implements Plugin { +public abstract class SmithyJavaPlugin implements Plugin { private static final String SMITHY_JAVA_GROUP = "software.amazon.smithy.java"; private static final String JAVA_CODEGEN_PLUGIN_NAME = "java-codegen"; @@ -61,6 +66,9 @@ public class SmithyJavaPlugin implements Plugin { private static final String CLEAN_SMITHY_OUTPUT_TASK_NAME = "cleanSmithyOutput"; private static final String MERGE_SERVICE_FILES_TASK_NAME = "mergeSmithyServiceFiles"; + @Inject + protected abstract FileSystemOperations getFileSystemOperations(); + @Override public void apply(Project project) { project.getPlugins().apply("software.amazon.smithy.gradle.smithy-base"); @@ -73,10 +81,13 @@ public void apply(Project project) { .getByType(SmithyExtension.class); configureDependencies(project, smithyExt, ext); - wireGeneratedSources(project, smithyExt, ext); configureCleanOutput(project, smithyExt); - configureTaskDependencies(project); - configureServiceFileMerging(project, smithyExt, ext); + + project.afterEvaluate(p -> { + wireGeneratedSources(p, smithyExt, ext); + configureTaskDependencies(p); + configureServiceFileMerging(p, smithyExt, ext); + }); }); project.afterEvaluate(p -> { @@ -92,11 +103,6 @@ private void configureDependencies( SmithyExtension smithyExt, SmithyJavaExtension ext ) { - Configuration smithyBuild = project.getConfigurations().getByName("smithyBuild"); - - // Resolve modes via ValueSource for configuration cache compatibility. - // If explicit modes are set in the DSL, use those directly; otherwise - // read smithy-build.json through a tracked ValueSource. Provider> configFiles = smithyExt.getSmithyBuildConfigs() .map(FileCollection::getFiles); Provider> inferredModes = project.getProviders().of( @@ -105,38 +111,45 @@ private void configureDependencies( Provider> modes = ext.getModes().map(declared -> declared.isEmpty() ? inferredModes.get() : declared); + Configuration smithyBuild = project.getConfigurations().getByName("smithyBuild"); smithyBuild.withDependencies(deps -> { if (!ext.getAutoAddDependencies().getOrElse(true)) { return; } - addIfAbsent(deps, project.getDependencies(), "codegen-plugin", SmithyJavaVersion.VERSION); + String version = SmithyJavaVersion.VERSION; + Set resolved = modes.get(); + addIfAbsent(deps, project.getDependencies(), "codegen-plugin", version); + if (resolved.contains("client")) { + addIfAbsent(deps, project.getDependencies(), "client-core", version); + } + if (resolved.contains("server")) { + addIfAbsent(deps, project.getDependencies(), "server-api", version); + } }); - // Wire runtime deps to implementation by default. The withDependencies callback - // checks at resolution time whether java-library has been applied (api exists) - // and skips deps that belong on api instead. Configuration implementation = project.getConfigurations().getByName("implementation"); implementation.withDependencies(deps -> { if (!ext.getAutoAddDependencies().getOrElse(true)) { return; } - boolean hasApi = project.getPlugins().hasPlugin(JavaLibraryPlugin.class); String version = SmithyJavaVersion.VERSION; Set resolved = modes.get(); - if (!hasApi) { - addIfAbsent(deps, project.getDependencies(), "core", version); - addIfAbsent(deps, project.getDependencies(), "framework-errors", version); - if (resolved.contains("client")) { - addIfAbsent(deps, project.getDependencies(), "client-core", version); - } + boolean hasApi = project.getPlugins().hasPlugin(JavaLibraryPlugin.class); + boolean serverOnly = resolved.contains("server") + && !resolved.contains("client") && !resolved.contains("types"); + if (hasApi && !serverOnly) { + return; + } + addIfAbsent(deps, project.getDependencies(), "core", version); + addIfAbsent(deps, project.getDependencies(), "framework-errors", version); + if (resolved.contains("client")) { + addIfAbsent(deps, project.getDependencies(), "client-core", version); } if (resolved.contains("server")) { addIfAbsent(deps, project.getDependencies(), "server-api", version); } }); - // When java-library is applied, types/client deps go to api. - // Use withType to react regardless of plugin application order. project.getPlugins().withType(JavaLibraryPlugin.class, plugin -> { Configuration api = project.getConfigurations().getByName("api"); api.withDependencies(deps -> { @@ -145,11 +158,19 @@ private void configureDependencies( } String version = SmithyJavaVersion.VERSION; Set resolved = modes.get(); + boolean serverOnly = resolved.contains("server") + && !resolved.contains("client") && !resolved.contains("types"); + if (serverOnly) { + return; + } addIfAbsent(deps, project.getDependencies(), "core", version); addIfAbsent(deps, project.getDependencies(), "framework-errors", version); if (resolved.contains("client")) { addIfAbsent(deps, project.getDependencies(), "client-core", version); } + if (resolved.contains("server")) { + addIfAbsent(deps, project.getDependencies(), "server-api", version); + } }); }); } @@ -161,14 +182,13 @@ private void wireGeneratedSources( ) { SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); sourceSets.named(SourceSet.MAIN_SOURCE_SET_NAME, sourceSet -> { - Provider projection = smithyExt.getSourceProjection(); + sourceSet.getJava().srcDir(project.files((Callable) () -> + resolveCodegenPaths(smithyExt, ext, "java"))); - Provider codegenPath = projection.flatMap( - p -> smithyExt.getPluginProjectionPath(p, JAVA_CODEGEN_PLUGIN_NAME)); - sourceSet.getJava().srcDir(codegenPath.map(p -> p.resolve("java").toFile())); - sourceSet.getResources().srcDir(codegenPath.map(p -> p.resolve("resources").toFile())); + sourceSet.getResources().srcDir(project.files((Callable) () -> + resolveCodegenPaths(smithyExt, ext, "resources"))); - // Callable defers evaluation until the source set is resolved, so generatedPluginOutputs is finalized + Provider projection = smithyExt.getSourceProjection(); sourceSet.getJava().srcDir(project.files((Callable) () -> ext.getGeneratedPluginOutputs().get().stream() .map(name -> smithyExt.getPluginProjectionPath( @@ -187,13 +207,16 @@ private void wireGeneratedSources( } private void configureCleanOutput(Project project, SmithyExtension smithyExt) { + Provider outputDirectory = smithyExt.getOutputDirectory().getAsFile(); project.getTasks().register(CLEAN_SMITHY_OUTPUT_TASK_NAME, Delete.class, task -> { task.setGroup("smithy"); - task.setDescription("Cleans the Smithy output directory before code generation"); - task.delete(smithyExt.getOutputDirectory()); + task.setDescription("Cleans the Smithy output directory"); + task.delete(outputDirectory); }); + FileSystemOperations fs = getFileSystemOperations(); project.getTasks().named(SMITHY_BUILD_TASK_NAME, task -> - task.dependsOn(CLEAN_SMITHY_OUTPUT_TASK_NAME)); + task.doFirst(CLEAN_SMITHY_OUTPUT_TASK_NAME, ignored -> + fs.delete(spec -> spec.delete(outputDirectory)))); } private void configureTaskDependencies(Project project) { @@ -215,16 +238,13 @@ private void configureServiceFileMerging( ) { Provider projection = smithyExt.getSourceProjection(); - Provider codegenPath = projection.flatMap( - p -> smithyExt.getPluginProjectionPath(p, JAVA_CODEGEN_PLUGIN_NAME)); - Provider codegenServicesDir = codegenPath.map( - p -> p.resolve("resources/META-INF/services").toFile()); - TaskProvider mergeTask = project.getTasks() .register(MERGE_SERVICE_FILES_TASK_NAME, MergeServiceFilesTask.class, task -> { task.dependsOn(SMITHY_BUILD_TASK_NAME); task.setGroup("smithy"); - task.getServiceDirectories().from(codegenServicesDir); + + task.getServiceDirectories().from(project.files((Callable) () -> + resolveCodegenPaths(smithyExt, ext, "resources/META-INF/services"))); task.getServiceDirectories().from(project.files((Callable) () -> ext.getGeneratedPluginOutputs().get().stream() @@ -235,9 +255,13 @@ private void configureServiceFileMerging( .toFile()) .collect(Collectors.toList()))); - task.onlyIf(t -> !ext.getGeneratedPluginOutputs() - .getOrElse(List.of()).isEmpty() - && ext.getMergeServiceFiles().getOrElse(true)); + task.onlyIf(t -> { + if (!ext.getMergeServiceFiles().getOrElse(true)) { + return false; + } + return ext.getProjections().get().size() > 1 + || !ext.getGeneratedPluginOutputs().getOrElse(List.of()).isEmpty(); + }); }); Provider mergedServicesDir = project.getLayout().getBuildDirectory() @@ -252,7 +276,6 @@ private void configureServiceFileMerging( } return List.of(); }), spec -> spec.into(".")); - // Files added via from() above bypass eachFile, so only originals are excluded task.eachFile(details -> { if (isMergingActive(ext) && details.getRelativePath().getPathString().startsWith("META-INF/services/") @@ -283,8 +306,24 @@ private void configureServiceFileMerging( } private static boolean isMergingActive(SmithyJavaExtension ext) { - return !ext.getGeneratedPluginOutputs().getOrElse(List.of()).isEmpty() - && ext.getMergeServiceFiles().getOrElse(true); + if (!ext.getMergeServiceFiles().getOrElse(true)) { + return false; + } + return ext.getProjections().get().size() > 1 + || !ext.getGeneratedPluginOutputs().getOrElse(List.of()).isEmpty(); + } + + private static List resolveCodegenPaths(SmithyExtension smithyExt, SmithyJavaExtension ext, String subpath) { + List explicitProjections = ext.getProjections().get(); + if (explicitProjections.isEmpty()) { + return List.of(smithyExt.getPluginProjectionPath( + smithyExt.getSourceProjection().get(), JAVA_CODEGEN_PLUGIN_NAME) + .get().resolve(subpath).toFile()); + } + return explicitProjections.stream() + .map(name -> smithyExt.getPluginProjectionPath(name, JAVA_CODEGEN_PLUGIN_NAME) + .get().resolve(subpath).toFile()) + .collect(Collectors.toList()); } private static void addIfAbsent( diff --git a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/tasks/MergeServiceFilesTask.java b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/tasks/MergeServiceFilesTask.java index dd2e44540c..7ccff97f18 100644 --- a/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/tasks/MergeServiceFilesTask.java +++ b/gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/tasks/MergeServiceFilesTask.java @@ -18,6 +18,7 @@ import org.gradle.api.GradleException; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileSystemOperations; import org.gradle.api.file.ProjectLayout; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.InputFiles; @@ -45,6 +46,9 @@ public MergeServiceFilesTask(ProjectLayout layout) { setDescription("Merges META-INF/services files from multiple Smithy build plugins."); } + @Inject + protected abstract FileSystemOperations getFileSystemOperations(); + /** * Directories containing {@code META-INF/services} files to merge. * @@ -97,7 +101,10 @@ public void merge() { } File outputDir = getOutputDirectory().getAsFile().get(); - if (!outputDir.mkdirs() && !outputDir.isDirectory()) { + if (outputDir.exists()) { + getFileSystemOperations().delete(spec -> spec.delete(outputDir)); + } + if (!outputDir.mkdirs()) { throw new GradleException("Failed to create output directory: " + outputDir); } diff --git a/mcp/mcp-schemas/build.gradle.kts b/mcp/mcp-schemas/build.gradle.kts index 1c3b2ed9b1..a694695950 100644 --- a/mcp/mcp-schemas/build.gradle.kts +++ b/mcp/mcp-schemas/build.gradle.kts @@ -10,18 +10,12 @@ extra["displayName"] = "Smithy :: Java :: MCP Schemas" extra["moduleName"] = "software.amazon.smithy.mcp.schemas" smithyJava { - autoAddDependencies = false generatedPluginOutputs.add("trait-codegen") } dependencies { - api(project(":core")) api(libs.smithy.model) - api(project(":server:server-api")) - api(project(":framework-errors")) api(project(":smithy-ai-traits")) - smithyBuild(project(":codegen:codegen-plugin")) - smithyBuild(project(":server:server-api")) smithyBuild(libs.smithy.traitcodegen) }