Skip to content

Commit e19d5a2

Browse files
committed
Avoid smithyBuild not being up-to-date incorrectly and use plugin everywhere
1 parent d5ea50a commit e19d5a2

10 files changed

Lines changed: 190 additions & 165 deletions

File tree

benchmarks/serde-benchmarks/build.gradle.kts

Lines changed: 14 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ plugins {
22
id("smithy-java.java-conventions")
33
id("com.gradleup.shadow")
44
id("smithy-java.jmh-conventions")
5-
id("software.amazon.smithy.gradle.smithy-base")
5+
id("software.amazon.smithy.java.gradle.smithy-java")
66
}
77

88
description = "Serde (serialization/deserialization) microbenchmarks for smithy-java codecs."
99

10-
// Not published. No `smithy-java.module-conventions`, no publishing, no BOM entry.
10+
smithyJava {
11+
projections.addAll(
12+
"aws-json-rpc-1-0-client",
13+
"aws-query-client",
14+
"rest-json-client",
15+
"rest-xml-client",
16+
"rpc-v2-cbor-client",
17+
)
18+
}
1119

1220
// Benchmarks intentionally target JDK 25 (the rest of smithy-java targets 21).
13-
// Performance measurements should reflect the latest JIT/runtime improvements
14-
// available to consumers; the runtime deps were compiled for 21 but run fine
15-
// on a newer JVM.
1621
java {
1722
toolchain {
1823
languageVersion = JavaLanguageVersion.of(25)
@@ -24,33 +29,18 @@ tasks.withType<JavaCompile>().configureEach {
2429
}
2530

2631
dependencies {
27-
// Smithy traits needed at build time (smithy-build) and at runtime (when
28-
// BenchmarkTestCases loads the model). The jmh configuration extends
29-
// implementation, so these are available to both.
3032
implementation(libs.smithy.model)
3133
implementation(libs.smithy.aws.traits)
3234
implementation(libs.smithy.protocol.traits)
3335
implementation(libs.smithy.protocol.test.traits)
3436
implementation(libs.smithy.utils)
3537

36-
// The Smithy Java codegen plugin produces typed shape classes plus
37-
// ApiOperation classes per service (see `smithy-build.json`). The
38-
// client-core dep is required because the generated client classes
39-
// reference it.
40-
smithyBuild(project(":codegen:codegen-plugin"))
41-
smithyBuild(project(":client:client-core"))
42-
43-
// smithy-java runtime stack — what we are benchmarking.
44-
jmh(project(":core"))
4538
jmh(project(":io"))
4639
jmh(project(":logging"))
4740
jmh(project(":codecs:json-codec", configuration = "shadow"))
4841
jmh(project(":codecs:cbor-codec"))
4942
jmh(project(":codecs:xml-codec"))
5043

51-
// Client protocols — every benchmark drives the corresponding
52-
// ClientProtocol#createRequest / #deserializeResponse, mirroring the
53-
// reference implementation's protocol-level entry points.
5444
jmh(project(":client:client-core"))
5545
jmh(project(":client:client-http"))
5646
jmh(project(":client:client-http-binding"))
@@ -60,7 +50,6 @@ dependencies {
6050
jmh(project(":aws:client:aws-client-restjson"))
6151
jmh(project(":aws:client:aws-client-restxml"))
6252

63-
// Protocol test document for converting test case params into typed shapes.
6453
jmh(project(":protocol-test-harness"))
6554

6655
// Dynamic-client path: build the ApiOperation + input from the runtime
@@ -69,20 +58,14 @@ dependencies {
6958
jmh(project(":dynamic-schemas"))
7059
}
7160

72-
// Smithy benchmark model files (tagged @httpRequestTests / @httpResponseTests
73-
// with the `serde-benchmark` tag).
74-
//
75-
// At runtime BenchmarkContext loads the model via
76-
// `Model.assembler().discoverModels()`, which walks `META-INF/smithy/manifest`
77-
// resources on the classpath.
7861
abstract class GenerateSmithyManifest : DefaultTask() {
7962
@get:InputDirectory
8063
abstract val sourceDir: DirectoryProperty
8164

8265
@get:OutputDirectory
8366
abstract val outputDir: DirectoryProperty
8467

85-
@org.gradle.api.tasks.TaskAction
68+
@TaskAction
8669
fun run() {
8770
val outRoot = outputDir.get().asFile
8871
val smithyDir = outRoot.resolve("META-INF/smithy")
@@ -109,50 +92,14 @@ val generateSmithyManifest by tasks.registering(GenerateSmithyManifest::class) {
10992
outputDir.set(layout.buildDirectory.dir("generated-resources/smithy-manifest"))
11093
}
11194

112-
// Wire each codegen projection's output into the jmh source set. There are
113-
// five projections (one per service); each emits Java source under
114-
// `build/smithyprojections/<project>/<projection>/java-codegen/java` into a
115-
// distinct package so same-named typed shapes from different protocols don't
116-
// collide.
117-
val codegenProjections =
118-
listOf(
119-
"aws-json-rpc-1-0-client",
120-
"aws-query-client",
121-
"rest-json-client",
122-
"rest-xml-client",
123-
"rpc-v2-cbor-client",
124-
)
125-
126-
afterEvaluate {
127-
val projectionPaths =
128-
codegenProjections.map { name ->
129-
smithy.getPluginProjectionPath(name, "java-codegen").get()
130-
}
131-
sourceSets.named("jmh") {
132-
java {
133-
projectionPaths.forEach { srcDir("$it/java") }
134-
}
135-
resources {
136-
projectionPaths.forEach { srcDir("$it/resources") }
137-
srcDir(generateSmithyManifest)
138-
}
95+
sourceSets.named("jmh") {
96+
resources {
97+
srcDir(generateSmithyManifest)
13998
}
14099
}
141100

142101
tasks.named("processJmhResources") {
143102
dependsOn(generateSmithyManifest)
144-
dependsOn("smithyBuild")
145-
}
146-
147-
// Multiple codegen projections each register META-INF/services/...SchemaIndex
148-
// (and other SPI files). Both should be on the runtime classpath; tell
149-
// processJmhResources to keep both entries by concatenating.
150-
tasks.named<Copy>("processJmhResources") {
151-
duplicatesStrategy = DuplicatesStrategy.INCLUDE
152-
}
153-
154-
tasks.named("compileJmhJava") {
155-
dependsOn("smithyBuild")
156103
}
157104

158105
// Test case: -Pjmh.testCaseId=rpcv2Cbor_PutItemRequest_BinaryData_S
@@ -181,23 +128,11 @@ jmh {
181128
resultsFile = layout.buildDirectory.file("results/jmh/results.json")
182129
}
183130

184-
// With shadow applied before jmh, jmhJar is a ShadowJar. Configure
185-
// mergeServiceFiles() so duplicate META-INF/services/ entries from
186-
// multiple codegen projections are concatenated rather than overwritten.
187131
tasks.jmhJar {
188132
mergeServiceFiles()
189133
append("META-INF/smithy/manifest")
190134
}
191135

192-
// Run the cross-language result converter. Reads the JMH JSON written by the
193-
// `jmh` task and writes a JSON + Markdown pair conforming to the shared
194-
// benchmark output schema. OS, instance type (via EC2 IMDS), and smithy-java
195-
// version are detected at runtime.
196-
//
197-
// ./gradlew :benchmarks:serde-benchmarks:convertJmhResults
198-
//
199-
// Optional properties:
200-
// -PoutputPrefix=<path> prefix for the output files (default: build/results/jmh/output)
201136
tasks.register<JavaExec>("convertJmhResults") {
202137
group = "benchmarks"
203138
description = "Convert JMH JSON output to the cross-language serde benchmark schema."

build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ val smithyJavaVersion = project.file("VERSION").readText().replace(System.lineSe
2525
allprojects {
2626
group = "software.amazon.smithy.java"
2727
version = smithyJavaVersion
28+
29+
configurations.all {
30+
resolutionStrategy.dependencySubstitution {
31+
rootProject.allprojects.forEach {
32+
substitute(module("${it.group}:${it.name}")).using(project(it.path))
33+
}
34+
}
35+
}
2836
}
2937
println("Smithy-Java version: '${smithyJavaVersion}'")
3038

examples/build.gradle.kts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11

2-
// Substitute any maven module dependencies with and project dependencies
32
subprojects {
43
group = "software.amazon.smithy.java.example"
54

6-
configurations.all {
7-
resolutionStrategy.dependencySubstitution {
8-
rootProject.allprojects.forEach {
9-
substitute(module("${it.group}:${it.name}")).using(project(it.path))
10-
}
11-
}
12-
}
13-
145
plugins.withId("java") {
156
the<JavaPluginExtension>().toolchain {
167
languageVersion.set(JavaLanguageVersion.of(21))

examples/end-to-end/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ dependencies {
1616

1717
// Client dependencies
1818
implementation("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion")
19-
implementation("software.amazon.smithy.java:client-core:$smithyJavaVersion")
2019

2120
// Test dependencies
2221
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")

gradle-plugin/README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,21 @@ When set, these override whatever is in `smithy-build.json` for dependency resol
197197

198198
When empty (default), modes are read from `smithy-build.json` automatically.
199199

200+
### `projections`
201+
202+
When your `smithy-build.json` uses named projections (instead of or in addition to the
203+
default source projection), list them here so the plugin wires each projection's
204+
`java-codegen` output into the main source set:
205+
206+
```kotlin
207+
smithyJava {
208+
projections.addAll("rest-json-client", "rpc-v2-cbor-client")
209+
}
210+
```
211+
212+
When empty (default), the plugin uses the source projection. Service files from multiple
213+
projections are merged automatically.
214+
200215
### `generatedPluginOutputs`
201216

202217
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
217232
dependencies based on the active modes. The configuration used depends on which Java plugin
218233
you apply and the codegen mode:
219234

220-
- **`java-library`**: types/client dependencies (`core`, `framework-errors`, `client-core`)
221-
are added to `api`, making them transitively available to consumers. Server dependencies
222-
(`server-api`) are added to `implementation` since servers are typically not re-exported.
235+
- **`java-library`**: all runtime dependencies are added to `api` unless the mode is
236+
server-only (no types or client), in which case `implementation` is used.
223237
- **`java` / `application`**: all runtime dependencies are added to `implementation`.
224238

225239
Set to `false` when you need full control over dependency versions or want to use project

gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyBuildModesValueSource.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.io.IOException;
1010
import java.nio.file.Files;
1111
import java.util.HashSet;
12+
import java.util.Locale;
1213
import java.util.Set;
1314
import org.gradle.api.provider.SetProperty;
1415
import org.gradle.api.provider.ValueSource;
@@ -35,28 +36,42 @@ public interface Params extends ValueSourceParameters {
3536

3637
@Override
3738
public Set<String> obtain() {
39+
Set<String> allModes = new HashSet<>();
3840
for (File config : getParameters().getSmithyBuildConfigs().get()) {
3941
if (!config.exists()) {
4042
continue;
4143
}
4244
try {
4345
String content = Files.readString(config.toPath());
4446
ObjectNode root = Node.parseJsonWithComments(content).expectObjectNode();
45-
return root.getObjectMember("plugins")
47+
48+
root.getObjectMember("plugins")
4649
.flatMap(plugins -> plugins.getObjectMember(JAVA_CODEGEN_PLUGIN_NAME))
4750
.flatMap(codegen -> codegen.getArrayMember("modes"))
4851
.map(SmithyBuildModesValueSource::extractModes)
49-
.orElse(Set.of("types"));
52+
.ifPresent(allModes::addAll);
53+
root.getObjectMember("projections").ifPresent(projections -> {
54+
for (Node value : projections.getMembers().values()) {
55+
value.expectObjectNode()
56+
.getObjectMember("plugins")
57+
.flatMap(plugins -> plugins.getObjectMember(JAVA_CODEGEN_PLUGIN_NAME))
58+
.flatMap(codegen -> codegen.getArrayMember("modes"))
59+
.map(SmithyBuildModesValueSource::extractModes)
60+
.ifPresent(allModes::addAll);
61+
}
62+
});
5063
} catch (IOException ignored) {
5164
}
5265
}
53-
return Set.of("types");
66+
return allModes.isEmpty() ? Set.of("types") : allModes;
5467
}
5568

5669
private static Set<String> extractModes(ArrayNode modesArray) {
5770
Set<String> modes = new HashSet<>();
5871
for (Node element : modesArray.getElements()) {
59-
element.asStringNode().map(StringNode::getValue).ifPresent(modes::add);
72+
element.asStringNode()
73+
.map(s -> s.getValue().toLowerCase(Locale.ENGLISH))
74+
.ifPresent(modes::add);
6075
}
6176
return modes;
6277
}

gradle-plugin/src/main/java/software/amazon/smithy/java/gradle/SmithyJavaExtension.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public abstract class SmithyJavaExtension {
2828
public SmithyJavaExtension() {
2929
getAutoAddDependencies().convention(true);
3030
getModes().convention(Collections.emptySet());
31+
getProjections().convention(Collections.emptyList());
3132
getGeneratedPluginOutputs().convention(Collections.emptyList());
3233
getMergeServiceFiles().convention(true);
3334
}
@@ -39,13 +40,14 @@ public SmithyJavaExtension() {
3940
* <ul>
4041
* <li>Always: {@code codegen-plugin} to smithyBuild; {@code core} and
4142
* {@code framework-errors} to api/implementation</li>
42-
* <li>Client mode: {@code client-core} to api/implementation</li>
43-
* <li>Server mode: {@code server-api} to implementation</li>
43+
* <li>Client mode: {@code client-core} to smithyBuild and api/implementation</li>
44+
* <li>Server mode: {@code server-api} to smithyBuild and api/implementation</li>
4445
* </ul>
4546
*
46-
* <p>When {@code java-library} is applied, types/client dependencies use {@code api}
47-
* and server dependencies use {@code implementation}. When only {@code java} or
48-
* {@code application} is applied, all dependencies use {@code implementation}.
47+
* <p>When {@code java-library} is applied, all runtime dependencies use {@code api}
48+
* unless the mode is server-only, in which case {@code implementation} is used.
49+
* When only {@code java} or {@code application} is applied, all dependencies use
50+
* {@code implementation}.
4951
*
5052
* <p>Set to {@code false} to manage all dependencies manually.
5153
*
@@ -73,6 +75,26 @@ public SmithyJavaExtension() {
7375
*/
7476
public abstract SetProperty<String> getModes();
7577

78+
/**
79+
* Explicit list of projection names whose {@code java-codegen} output should be
80+
* wired into the main source set. When non-empty, these are used instead of
81+
* the default source projection.
82+
*
83+
* <p>This is useful for multi-projection builds where each projection generates
84+
* code for a different service or protocol:
85+
* <pre>{@code
86+
* smithyJava {
87+
* projections.addAll("rest-json-client", "rpc-v2-cbor-client")
88+
* }
89+
* }</pre>
90+
*
91+
* <p>When empty (default), the plugin uses the single source projection from
92+
* the {@code smithy} extension.
93+
*
94+
* @return list of projection names to wire
95+
*/
96+
public abstract ListProperty<String> getProjections();
97+
7698
/**
7799
* Additional Smithy build plugin names (as declared in {@code smithy-build.json})
78100
* whose generated output directories should be wired into the Java source set.
@@ -87,10 +109,11 @@ public SmithyJavaExtension() {
87109
public abstract ListProperty<String> getGeneratedPluginOutputs();
88110

89111
/**
90-
* Whether to automatically merge {@code META-INF/services} files when
91-
* {@link #getGeneratedPluginOutputs()} is non-empty. Defaults to {@code true}.
112+
* Whether to automatically merge {@code META-INF/services} files when multiple
113+
* projections or {@link #getGeneratedPluginOutputs()} are configured. Defaults to
114+
* {@code true}.
92115
*
93-
* <p>When multiple Smithy build plugins produce service provider files, they
116+
* <p>When multiple projections or plugins produce service provider files, they
94117
* may conflict. This option enables a merge task that combines them.
95118
*
96119
* @return property controlling service file merging

0 commit comments

Comments
 (0)