From c5336facb65d617807d29d23da94fa5e6551dcde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Fri, 1 May 2026 14:09:12 -0700 Subject: [PATCH 1/7] Add a mechanism to avoid mixing versions for a client --- .../kotlin/GenerateVersionProviderTask.kt | 33 +++++++ .../smithy-java.module-conventions.gradle.kts | 8 ++ .../VersionCompatibilityIntegration.java | 54 ++++++++++++ .../integrations/version/package-info.java | 4 + ...smithy.java.codegen.JavaCodegenIntegration | 1 + ...ithyVersionCompatibilityTest.java.template | 87 +++++++++++++++++++ .../java/codegen/types/CodegenTest.java | 17 ++-- .../build.gradle.kts | 11 +++ 8 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 buildSrc/src/main/kotlin/GenerateVersionProviderTask.kt create mode 100644 codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java create mode 100644 codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/package-info.java create mode 100644 codegen/codegen-core/src/main/resources/software/amazon/smithy/java/codegen/integrations/version/SmithyVersionCompatibilityTest.java.template diff --git a/buildSrc/src/main/kotlin/GenerateVersionProviderTask.kt b/buildSrc/src/main/kotlin/GenerateVersionProviderTask.kt new file mode 100644 index 000000000..cfb4d664f --- /dev/null +++ b/buildSrc/src/main/kotlin/GenerateVersionProviderTask.kt @@ -0,0 +1,33 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import java.io.File + +/** + * Gradle task that generates a version marker resource for a module. + * + * Each module gets a {@code META-INF/smithy-java/versions.properties} file + * containing the module name and version. At test time, all such files are + * discovered via {@code ClassLoader.getResources()} to validate version consistency. + */ +abstract class GenerateVersionProviderTask : DefaultTask() { + + @get:Input + var moduleName: String = "" + + @get:Input + var moduleVersion: String = "" + + @get:OutputDirectory + var outputDir: File = project.layout.buildDirectory.dir("generated/version-provider").get().asFile + + @TaskAction + fun generate() { + val dir = File(outputDir, "resources/META-INF/smithy-java") + dir.mkdirs() + File(dir, "versions.properties").writeText( + "module=$moduleName\nversion=$moduleVersion\n" + ) + } +} diff --git a/buildSrc/src/main/kotlin/smithy-java.module-conventions.gradle.kts b/buildSrc/src/main/kotlin/smithy-java.module-conventions.gradle.kts index 3a53f7f10..00b1dacba 100644 --- a/buildSrc/src/main/kotlin/smithy-java.module-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/smithy-java.module-conventions.gradle.kts @@ -50,6 +50,14 @@ afterEvaluate { attributes(mapOf("Automatic-Module-Name" to moduleName)) } } + + // Generate a version marker resource for this module. + val generateVersionProvider = tasks.register("generateVersionProvider") { + this.moduleName = moduleName + this.moduleVersion = smithyJavaVersion + } + sourceSets["main"].resources.srcDir(generateVersionProvider.map { it.outputDir.resolve("resources") }) + tasks.named("processResources") { dependsOn(generateVersionProvider) } } // Always run javadoc after build. diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java new file mode 100644 index 000000000..5c5746838 --- /dev/null +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.integrations.version; + +import software.amazon.smithy.java.codegen.CodeGenerationContext; +import software.amazon.smithy.java.codegen.JavaCodegenIntegration; +import software.amazon.smithy.java.core.Version; +import software.amazon.smithy.utils.IoUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Codegen integration that generates a test to validate that all Smithy Java + * modules on the classpath have compatible versions. + * + *

Each Smithy Java module includes a resource at + * {@code META-INF/smithy-java/versions.properties} containing its module name + * and version. Since each JAR has its own copy, {@code ClassLoader.getResources()} + * returns one URL per module. The generated test reads all of them and asserts: + *

    + *
  1. All modules report the same version (strict equality).
  2. + *
  3. All module versions are ≥ the version the code was generated against.
  4. + *
+ */ +@SmithyInternalApi +public final class VersionCompatibilityIntegration implements JavaCodegenIntegration { + + private static final String CLASS_NAME = "SmithyVersionCompatibilityTest"; + private static final String TEMPLATE = IoUtils.readUtf8Resource( + VersionCompatibilityIntegration.class, + CLASS_NAME + ".java.template" + ); + + @Override + public String name() { + return "version-compatibility"; + } + + @Override + public void customize(CodeGenerationContext codegenContext) { + var settings = codegenContext.settings(); + var packagePath = settings.packageNamespace().replace(".", "/"); + var testFilePath = "test-java/" + packagePath + "/" + CLASS_NAME + ".java"; + + var content = TEMPLATE + .replace("${VERSION}", Version.VERSION); + + codegenContext.writerDelegator() + .useFileWriter(testFilePath, settings.packageNamespace(), + writer -> writer.writeInlineWithNoFormatting(content)); + } +} diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/package-info.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/package-info.java new file mode 100644 index 000000000..7f5872318 --- /dev/null +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/package-info.java @@ -0,0 +1,4 @@ +/** + * Version compatibility codegen integration. + */ +package software.amazon.smithy.java.codegen.integrations.version; diff --git a/codegen/codegen-core/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration b/codegen/codegen-core/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration index 71252e369..892d1eeea 100644 --- a/codegen/codegen-core/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration +++ b/codegen/codegen-core/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration @@ -1,2 +1,3 @@ software.amazon.smithy.java.codegen.integrations.core.CoreIntegration software.amazon.smithy.java.codegen.integrations.javadoc.JavadocIntegration +software.amazon.smithy.java.codegen.integrations.version.VersionCompatibilityIntegration diff --git a/codegen/codegen-core/src/main/resources/software/amazon/smithy/java/codegen/integrations/version/SmithyVersionCompatibilityTest.java.template b/codegen/codegen-core/src/main/resources/software/amazon/smithy/java/codegen/integrations/version/SmithyVersionCompatibilityTest.java.template new file mode 100644 index 000000000..fb0974752 --- /dev/null +++ b/codegen/codegen-core/src/main/resources/software/amazon/smithy/java/codegen/integrations/version/SmithyVersionCompatibilityTest.java.template @@ -0,0 +1,87 @@ +import java.io.IOException; +import java.util.ArrayList; +import java.util.Properties; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Auto-generated test that validates all Smithy Java modules on the classpath + * have compatible versions. Generated against version {@code ${VERSION}}. + */ +public final class SmithyVersionCompatibilityTest { + private static final String GENERATED_VERSION = "${VERSION}"; + private static final String VERSIONS_RESOURCE = "META-INF/smithy-java/versions.properties"; + + @Test + void allSmithyModulesHaveCompatibleVersions() throws IOException { + var modules = new ArrayList(); + var urls = Thread.currentThread() + .getContextClassLoader() + .getResources(VERSIONS_RESOURCE); + + while (urls.hasMoreElements()) { + var props = new Properties(); + try (var is = urls.nextElement().openStream()) { + props.load(is); + } + modules.add(new String[]{ + props.getProperty("module", "unknown"), + props.getProperty("version", "unknown") + }); + } + + if (modules.isEmpty()) { + fail("No Smithy Java version markers found on the classpath. " + + "Ensure smithy-java modules are properly included."); + } + + var errors = new ArrayList(); + + // All modules must report the same version. + var firstVersion = modules.get(0)[1]; + for (var module : modules) { + if (!module[1].equals(firstVersion)) { + errors.add("Version mismatch: module '" + + modules.get(0)[0] + "' has version " + firstVersion + + " but module '" + module[0] + "' has version " + module[1]); + } + } + + // All module versions must be >= the codegen version. + for (var module : modules) { + if (compareVersions(module[1], GENERATED_VERSION) < 0) { + errors.add("Module '" + module[0] + "' version " + + module[1] + " is older than the codegen version " + + GENERATED_VERSION); + } + } + + if (!errors.isEmpty()) { + fail("Smithy Java version compatibility check failed:\n - " + + String.join("\n - ", errors)); + } + } + + private static int compareVersions(String v1, String v2) { + var parts1 = v1.split("[.\\-]"); + var parts2 = v2.split("[.\\-]"); + var len = Math.max(parts1.length, parts2.length); + for (int i = 0; i < len; i++) { + var p1 = i < parts1.length ? parsePart(parts1[i]) : 0; + var p2 = i < parts2.length ? parsePart(parts2[i]) : 0; + if (p1 != p2) { + return Integer.compare(p1, p2); + } + } + return 0; + } + + private static int parsePart(String part) { + try { + return Integer.parseInt(part); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java index 9e2bfc5c1..d61a59725 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java @@ -49,7 +49,7 @@ void expectedFilesExist() { var context = contextBuilder.settings(settings).build(); plugin.execute(context); assertThat(manifest.getFiles()) - .hasSize(8) + .hasSize(9) .containsExactlyInAnyOrder( Path.of("/java/test/smithy/codegen/types/test/model/EnumShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/IntEnumShape.java"), @@ -58,7 +58,8 @@ void expectedFilesExist() { Path.of("/java/test/smithy/codegen/types/test/model/StructureShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/UnionShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/GeneratedSchemaIndex.java"), - Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex")); + Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex"), + Path.of("/test-java/test/smithy/codegen/types/test/SmithyVersionCompatibilityTest.java")); } @Test @@ -69,13 +70,14 @@ void respectsSelector() { var context = contextBuilder.settings(settings).build(); plugin.execute(context); assertThat(manifest.getFiles()) - .hasSize(5) + .hasSize(6) .containsExactlyInAnyOrder( Path.of("/java/test/smithy/codegen/types/test/model/Schemas.java"), Path.of("/java/test/smithy/codegen/types/test/model/SharedSerde.java"), Path.of("/java/test/smithy/codegen/types/test/model/StructureShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/GeneratedSchemaIndex.java"), - Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex")); + Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex"), + Path.of("/test-java/test/smithy/codegen/types/test/SmithyVersionCompatibilityTest.java")); } @Test @@ -86,16 +88,17 @@ void specificShapesAdded() { .build(); var context = contextBuilder.settings(settings).build(); plugin.execute(context); - assertEquals(6, manifest.getFiles().size()); + assertEquals(7, manifest.getFiles().size()); assertThat(manifest.getFiles()) - .hasSize(6) + .hasSize(7) .containsExactlyInAnyOrder( Path.of("/java/test/smithy/codegen/types/test/model/Schemas.java"), Path.of("/java/test/smithy/codegen/types/test/model/SharedSerde.java"), Path.of("/java/test/smithy/codegen/types/test/model/StructureShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/UnionShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/GeneratedSchemaIndex.java"), - Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex")); + Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex"), + Path.of("/test-java/test/smithy/codegen/types/test/SmithyVersionCompatibilityTest.java")); } } diff --git a/examples/transcribestreaming-client/build.gradle.kts b/examples/transcribestreaming-client/build.gradle.kts index b8613b92a..da3af397f 100644 --- a/examples/transcribestreaming-client/build.gradle.kts +++ b/examples/transcribestreaming-client/build.gradle.kts @@ -38,6 +38,11 @@ afterEvaluate { srcDir("$clientPath/resources") } } + test { + java { + srcDir("$clientPath/test-java") + } + } create("it") { compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output @@ -50,9 +55,15 @@ tasks { compileJava { dependsOn(smithyBuild) } + compileTestJava { + dependsOn(smithyBuild) + } processResources { dependsOn(smithyBuild) } + test { + useJUnitPlatform() + } val integ by registering(Test::class) { useJUnitPlatform() testClassesDirs = sourceSets["it"].output.classesDirs From 0558dc9be74e44363c2fac36c5f9b9f54d8b5876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Fri, 1 May 2026 17:29:43 -0700 Subject: [PATCH 2/7] Run spotlessApply --- .../version/VersionCompatibilityIntegration.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java index 5c5746838..22a1ee5c9 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java @@ -29,9 +29,8 @@ public final class VersionCompatibilityIntegration implements JavaCodegenIntegra private static final String CLASS_NAME = "SmithyVersionCompatibilityTest"; private static final String TEMPLATE = IoUtils.readUtf8Resource( - VersionCompatibilityIntegration.class, - CLASS_NAME + ".java.template" - ); + VersionCompatibilityIntegration.class, + CLASS_NAME + ".java.template"); @Override public String name() { @@ -45,10 +44,11 @@ public void customize(CodeGenerationContext codegenContext) { var testFilePath = "test-java/" + packagePath + "/" + CLASS_NAME + ".java"; var content = TEMPLATE - .replace("${VERSION}", Version.VERSION); + .replace("${VERSION}", Version.VERSION); codegenContext.writerDelegator() - .useFileWriter(testFilePath, settings.packageNamespace(), - writer -> writer.writeInlineWithNoFormatting(content)); + .useFileWriter(testFilePath, + settings.packageNamespace(), + writer -> writer.writeInlineWithNoFormatting(content)); } } From a632ec0d074a18cb72cfadf570bf0b422214c1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Mon, 4 May 2026 17:21:52 -0700 Subject: [PATCH 3/7] Move the version validation to the Schemas class --- .../codegen/generators/SchemasGenerator.java | 16 ++- .../VersionCompatibilityIntegration.java | 54 --------- .../integrations/version/package-info.java | 4 - ...smithy.java.codegen.JavaCodegenIntegration | 1 - ...ithyVersionCompatibilityTest.java.template | 87 -------------- .../java/codegen/types/CodegenTest.java | 17 ++- .../core/IncompatibleVersionException.java | 15 +++ .../amazon/smithy/java/core/VersionCheck.java | 111 ++++++++++++++++++ .../build.gradle.kts | 11 -- 9 files changed, 148 insertions(+), 168 deletions(-) delete mode 100644 codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java delete mode 100644 codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/package-info.java delete mode 100644 codegen/codegen-core/src/main/resources/software/amazon/smithy/java/codegen/integrations/version/SmithyVersionCompatibilityTest.java.template create mode 100644 core/src/main/java/software/amazon/smithy/java/core/IncompatibleVersionException.java create mode 100644 core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java index 8b24359f1..a835b32b1 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java @@ -15,6 +15,8 @@ import software.amazon.smithy.java.codegen.JavaCodegenSettings; import software.amazon.smithy.java.codegen.generators.SchemaFieldOrder.SchemaField; import software.amazon.smithy.java.codegen.writer.JavaWriter; +import software.amazon.smithy.java.core.Version; +import software.amazon.smithy.java.core.VersionCheck; import software.amazon.smithy.java.core.schema.Schema; import software.amazon.smithy.java.core.schema.SchemaBuilder; import software.amazon.smithy.model.Model; @@ -51,9 +53,12 @@ public final class SchemasGenerator public void accept(CustomizeDirective directive) { var order = directive.context().schemaFieldOrder(); + boolean first = true; for (var shapeOrder : order.partitions()) { var className = shapeOrder.getFirst().classRef().className(); var fileName = CodegenUtils.getJavaFilePath(directive.settings(), "model", className); + boolean isFirst = first; + first = false; directive.context() .writerDelegator() .useFileWriter(fileName, CodegenUtils.getModelNamespace(directive.settings()), writer -> { @@ -63,7 +68,10 @@ public void accept(CustomizeDirective writer.write("$T.check($S);", + VersionCheck.class, + Version.VERSION)); + } writer.write(template); writer.popState(); }); diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java deleted file mode 100644 index 22a1ee5c9..000000000 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/VersionCompatibilityIntegration.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.codegen.integrations.version; - -import software.amazon.smithy.java.codegen.CodeGenerationContext; -import software.amazon.smithy.java.codegen.JavaCodegenIntegration; -import software.amazon.smithy.java.core.Version; -import software.amazon.smithy.utils.IoUtils; -import software.amazon.smithy.utils.SmithyInternalApi; - -/** - * Codegen integration that generates a test to validate that all Smithy Java - * modules on the classpath have compatible versions. - * - *

Each Smithy Java module includes a resource at - * {@code META-INF/smithy-java/versions.properties} containing its module name - * and version. Since each JAR has its own copy, {@code ClassLoader.getResources()} - * returns one URL per module. The generated test reads all of them and asserts: - *

    - *
  1. All modules report the same version (strict equality).
  2. - *
  3. All module versions are ≥ the version the code was generated against.
  4. - *
- */ -@SmithyInternalApi -public final class VersionCompatibilityIntegration implements JavaCodegenIntegration { - - private static final String CLASS_NAME = "SmithyVersionCompatibilityTest"; - private static final String TEMPLATE = IoUtils.readUtf8Resource( - VersionCompatibilityIntegration.class, - CLASS_NAME + ".java.template"); - - @Override - public String name() { - return "version-compatibility"; - } - - @Override - public void customize(CodeGenerationContext codegenContext) { - var settings = codegenContext.settings(); - var packagePath = settings.packageNamespace().replace(".", "/"); - var testFilePath = "test-java/" + packagePath + "/" + CLASS_NAME + ".java"; - - var content = TEMPLATE - .replace("${VERSION}", Version.VERSION); - - codegenContext.writerDelegator() - .useFileWriter(testFilePath, - settings.packageNamespace(), - writer -> writer.writeInlineWithNoFormatting(content)); - } -} diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/package-info.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/package-info.java deleted file mode 100644 index 7f5872318..000000000 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/version/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Version compatibility codegen integration. - */ -package software.amazon.smithy.java.codegen.integrations.version; diff --git a/codegen/codegen-core/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration b/codegen/codegen-core/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration index 892d1eeea..71252e369 100644 --- a/codegen/codegen-core/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration +++ b/codegen/codegen-core/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration @@ -1,3 +1,2 @@ software.amazon.smithy.java.codegen.integrations.core.CoreIntegration software.amazon.smithy.java.codegen.integrations.javadoc.JavadocIntegration -software.amazon.smithy.java.codegen.integrations.version.VersionCompatibilityIntegration diff --git a/codegen/codegen-core/src/main/resources/software/amazon/smithy/java/codegen/integrations/version/SmithyVersionCompatibilityTest.java.template b/codegen/codegen-core/src/main/resources/software/amazon/smithy/java/codegen/integrations/version/SmithyVersionCompatibilityTest.java.template deleted file mode 100644 index fb0974752..000000000 --- a/codegen/codegen-core/src/main/resources/software/amazon/smithy/java/codegen/integrations/version/SmithyVersionCompatibilityTest.java.template +++ /dev/null @@ -1,87 +0,0 @@ -import java.io.IOException; -import java.util.ArrayList; -import java.util.Properties; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.fail; - -/** - * Auto-generated test that validates all Smithy Java modules on the classpath - * have compatible versions. Generated against version {@code ${VERSION}}. - */ -public final class SmithyVersionCompatibilityTest { - private static final String GENERATED_VERSION = "${VERSION}"; - private static final String VERSIONS_RESOURCE = "META-INF/smithy-java/versions.properties"; - - @Test - void allSmithyModulesHaveCompatibleVersions() throws IOException { - var modules = new ArrayList(); - var urls = Thread.currentThread() - .getContextClassLoader() - .getResources(VERSIONS_RESOURCE); - - while (urls.hasMoreElements()) { - var props = new Properties(); - try (var is = urls.nextElement().openStream()) { - props.load(is); - } - modules.add(new String[]{ - props.getProperty("module", "unknown"), - props.getProperty("version", "unknown") - }); - } - - if (modules.isEmpty()) { - fail("No Smithy Java version markers found on the classpath. " - + "Ensure smithy-java modules are properly included."); - } - - var errors = new ArrayList(); - - // All modules must report the same version. - var firstVersion = modules.get(0)[1]; - for (var module : modules) { - if (!module[1].equals(firstVersion)) { - errors.add("Version mismatch: module '" - + modules.get(0)[0] + "' has version " + firstVersion - + " but module '" + module[0] + "' has version " + module[1]); - } - } - - // All module versions must be >= the codegen version. - for (var module : modules) { - if (compareVersions(module[1], GENERATED_VERSION) < 0) { - errors.add("Module '" + module[0] + "' version " - + module[1] + " is older than the codegen version " - + GENERATED_VERSION); - } - } - - if (!errors.isEmpty()) { - fail("Smithy Java version compatibility check failed:\n - " - + String.join("\n - ", errors)); - } - } - - private static int compareVersions(String v1, String v2) { - var parts1 = v1.split("[.\\-]"); - var parts2 = v2.split("[.\\-]"); - var len = Math.max(parts1.length, parts2.length); - for (int i = 0; i < len; i++) { - var p1 = i < parts1.length ? parsePart(parts1[i]) : 0; - var p2 = i < parts2.length ? parsePart(parts2[i]) : 0; - if (p1 != p2) { - return Integer.compare(p1, p2); - } - } - return 0; - } - - private static int parsePart(String part) { - try { - return Integer.parseInt(part); - } catch (NumberFormatException e) { - return 0; - } - } -} diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java index d61a59725..9e2bfc5c1 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java @@ -49,7 +49,7 @@ void expectedFilesExist() { var context = contextBuilder.settings(settings).build(); plugin.execute(context); assertThat(manifest.getFiles()) - .hasSize(9) + .hasSize(8) .containsExactlyInAnyOrder( Path.of("/java/test/smithy/codegen/types/test/model/EnumShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/IntEnumShape.java"), @@ -58,8 +58,7 @@ void expectedFilesExist() { Path.of("/java/test/smithy/codegen/types/test/model/StructureShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/UnionShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/GeneratedSchemaIndex.java"), - Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex"), - Path.of("/test-java/test/smithy/codegen/types/test/SmithyVersionCompatibilityTest.java")); + Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex")); } @Test @@ -70,14 +69,13 @@ void respectsSelector() { var context = contextBuilder.settings(settings).build(); plugin.execute(context); assertThat(manifest.getFiles()) - .hasSize(6) + .hasSize(5) .containsExactlyInAnyOrder( Path.of("/java/test/smithy/codegen/types/test/model/Schemas.java"), Path.of("/java/test/smithy/codegen/types/test/model/SharedSerde.java"), Path.of("/java/test/smithy/codegen/types/test/model/StructureShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/GeneratedSchemaIndex.java"), - Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex"), - Path.of("/test-java/test/smithy/codegen/types/test/SmithyVersionCompatibilityTest.java")); + Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex")); } @Test @@ -88,17 +86,16 @@ void specificShapesAdded() { .build(); var context = contextBuilder.settings(settings).build(); plugin.execute(context); - assertEquals(7, manifest.getFiles().size()); + assertEquals(6, manifest.getFiles().size()); assertThat(manifest.getFiles()) - .hasSize(7) + .hasSize(6) .containsExactlyInAnyOrder( Path.of("/java/test/smithy/codegen/types/test/model/Schemas.java"), Path.of("/java/test/smithy/codegen/types/test/model/SharedSerde.java"), Path.of("/java/test/smithy/codegen/types/test/model/StructureShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/UnionShape.java"), Path.of("/java/test/smithy/codegen/types/test/model/GeneratedSchemaIndex.java"), - Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex"), - Path.of("/test-java/test/smithy/codegen/types/test/SmithyVersionCompatibilityTest.java")); + Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex")); } } diff --git a/core/src/main/java/software/amazon/smithy/java/core/IncompatibleVersionException.java b/core/src/main/java/software/amazon/smithy/java/core/IncompatibleVersionException.java new file mode 100644 index 000000000..47059f1fc --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/IncompatibleVersionException.java @@ -0,0 +1,15 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.core; + +/** + * Thrown when incompatible versions of Smithy Java modules are detected on the classpath. + */ +public final class IncompatibleVersionException extends RuntimeException { + IncompatibleVersionException(String message) { + super(message); + } +} diff --git a/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java b/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java new file mode 100644 index 000000000..e22bc97f6 --- /dev/null +++ b/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.core; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Properties; + +/** + * Validates that all Smithy Java modules on the classpath have compatible versions. + * + *

This check runs once during class initialization of generated code. It discovers + * all {@code META-INF/smithy-java/versions.properties} resources on the classpath and + * verifies that all modules report the same version and that all versions are at least + * as new as the version the code was generated against. + * + *

The check can be disabled by setting the system property + * {@code smithy.java.skipVersionCheck} to {@code true}. + */ +public final class VersionCheck { + private static final String VERSIONS_RESOURCE = "META-INF/smithy-java/versions.properties"; + private static final String SKIP_PROPERTY = "smithy.java.skipVersionCheck"; + + private VersionCheck() {} + + /** + * Validates version compatibility of all Smithy Java modules on the classpath. + * + * @param codegenVersion the version the code was generated against + * @throws IncompatibleVersionException if a version mismatch is detected + */ + public static void check(String codegenVersion) { + if (Boolean.getBoolean(SKIP_PROPERTY)) { + return; + } + + var modules = new ArrayList(); + try { + var urls = Thread.currentThread() + .getContextClassLoader() + .getResources(VERSIONS_RESOURCE); + while (urls.hasMoreElements()) { + var props = new Properties(); + try (var is = urls.nextElement().openStream()) { + props.load(is); + } + modules.add(new String[] { + props.getProperty("module", "unknown"), + props.getProperty("version", "unknown") + }); + } + } catch (IOException e) { + // Don't fail startup if we can't read version resources. + return; + } + + if (modules.isEmpty()) { + return; + } + + var errors = new ArrayList(); + + // All modules must report the same version. + var firstVersion = modules.get(0)[1]; + for (var module : modules) { + if (!module[1].equals(firstVersion)) { + errors.add("Version mismatch: module '" + modules.get(0)[0] + "' has version " + + firstVersion + " but module '" + module[0] + "' has version " + module[1]); + } + } + + // All module versions must be >= the codegen version. + for (var module : modules) { + if (compareVersions(module[1], codegenVersion) < 0) { + errors.add("Module '" + module[0] + "' version " + module[1] + + " is older than the codegen version " + codegenVersion); + } + } + + if (!errors.isEmpty()) { + throw new IncompatibleVersionException( + "Smithy Java version compatibility check failed:\n - " + + String.join("\n - ", errors)); + } + } + + private static int compareVersions(String v1, String v2) { + var parts1 = v1.split("[.\\-]"); + var parts2 = v2.split("[.\\-]"); + var len = Math.max(parts1.length, parts2.length); + for (int i = 0; i < len; i++) { + var p1 = i < parts1.length ? parsePart(parts1[i]) : 0; + var p2 = i < parts2.length ? parsePart(parts2[i]) : 0; + if (p1 != p2) { + return Integer.compare(p1, p2); + } + } + return 0; + } + + private static int parsePart(String part) { + try { + return Integer.parseInt(part); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/examples/transcribestreaming-client/build.gradle.kts b/examples/transcribestreaming-client/build.gradle.kts index da3af397f..b8613b92a 100644 --- a/examples/transcribestreaming-client/build.gradle.kts +++ b/examples/transcribestreaming-client/build.gradle.kts @@ -38,11 +38,6 @@ afterEvaluate { srcDir("$clientPath/resources") } } - test { - java { - srcDir("$clientPath/test-java") - } - } create("it") { compileClasspath += main.get().output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] runtimeClasspath += output + compileClasspath + test.get().runtimeClasspath + test.get().output @@ -55,15 +50,9 @@ tasks { compileJava { dependsOn(smithyBuild) } - compileTestJava { - dependsOn(smithyBuild) - } processResources { dependsOn(smithyBuild) } - test { - useJUnitPlatform() - } val integ by registering(Test::class) { useJUnitPlatform() testClassesDirs = sourceSets["it"].output.classesDirs From 2bba88824864f7ef381c3d836328a0fe68135c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Mon, 4 May 2026 18:14:01 -0700 Subject: [PATCH 4/7] Make sure that the class & errors provide teh needed details to troubleshoot --- .../amazon/smithy/java/core/VersionCheck.java | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java b/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java index e22bc97f6..9bfc4ce23 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java +++ b/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java @@ -8,10 +8,17 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Properties; +import software.amazon.smithy.java.logging.InternalLogger; /** * Validates that all Smithy Java modules on the classpath have compatible versions. * + *

Mixing different versions of Smithy Java modules in the same application can cause + * subtle runtime errors such as missing methods, class not found exceptions, or unexpected + * behavior that are difficult to diagnose. This commonly happens when different dependencies + * pull in different versions of the same module transitively. This check detects such + * mismatches early, at class-load time, before any operation is executed. + * *

This check runs once during class initialization of generated code. It discovers * all {@code META-INF/smithy-java/versions.properties} resources on the classpath and * verifies that all modules report the same version and that all versions are at least @@ -21,6 +28,7 @@ * {@code smithy.java.skipVersionCheck} to {@code true}. */ public final class VersionCheck { + private static final InternalLogger LOGGER = InternalLogger.getLogger(VersionCheck.class); private static final String VERSIONS_RESOURCE = "META-INF/smithy-java/versions.properties"; private static final String SKIP_PROPERTY = "smithy.java.skipVersionCheck"; @@ -34,6 +42,10 @@ private VersionCheck() {} */ public static void check(String codegenVersion) { if (Boolean.getBoolean(SKIP_PROPERTY)) { + LOGGER.warn("Smithy Java version compatibility check is disabled via '{}'. " + + "This is not recommended and should only be used as a temporary workaround. " + + "Running with mismatched module versions may cause unexpected runtime errors.", + SKIP_PROPERTY); return; } @@ -81,9 +93,22 @@ public static void check(String codegenVersion) { } if (!errors.isEmpty()) { - throw new IncompatibleVersionException( - "Smithy Java version compatibility check failed:\n - " - + String.join("\n - ", errors)); + // Build a nice error message to give the end-user all the details needed + // to fix the issue. + var sb = new StringBuilder("Smithy Java version compatibility check failed:\n"); + sb.append(" Generated with version: ").append(codegenVersion).append("\n"); + sb.append(" Modules on classpath:\n"); + for (var module : modules) { + sb.append(" - ").append(module[0]).append(" = ").append(module[1]).append("\n"); + } + sb.append(" Issues:\n"); + for (var error : errors) { + sb.append(" - ").append(error).append("\n"); + } + sb.append(" Fix: Align all smithy-java dependencies to the same version. ") + .append("If using Gradle, consider importing the BOM: ") + .append("platform('software.amazon.smithy.java:bom:").append(codegenVersion).append("')"); + throw new IncompatibleVersionException(sb.toString()); } } From fc4d2e145533c4196b5472c652841103e19acf1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Mon, 4 May 2026 18:27:28 -0700 Subject: [PATCH 5/7] Run spotlessApply to fix formatting --- .../amazon/smithy/java/core/VersionCheck.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java b/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java index 9bfc4ce23..9db6e4634 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java +++ b/core/src/main/java/software/amazon/smithy/java/core/VersionCheck.java @@ -43,9 +43,9 @@ private VersionCheck() {} public static void check(String codegenVersion) { if (Boolean.getBoolean(SKIP_PROPERTY)) { LOGGER.warn("Smithy Java version compatibility check is disabled via '{}'. " - + "This is not recommended and should only be used as a temporary workaround. " - + "Running with mismatched module versions may cause unexpected runtime errors.", - SKIP_PROPERTY); + + "This is not recommended and should only be used as a temporary workaround. " + + "Running with mismatched module versions may cause unexpected runtime errors.", + SKIP_PROPERTY); return; } @@ -106,8 +106,10 @@ public static void check(String codegenVersion) { sb.append(" - ").append(error).append("\n"); } sb.append(" Fix: Align all smithy-java dependencies to the same version. ") - .append("If using Gradle, consider importing the BOM: ") - .append("platform('software.amazon.smithy.java:bom:").append(codegenVersion).append("')"); + .append("If using Gradle, consider importing the BOM: ") + .append("platform('software.amazon.smithy.java:bom:") + .append(codegenVersion) + .append("')"); throw new IncompatibleVersionException(sb.toString()); } } From 9a07ab1bea2c8f8bd73b438bf633086275aa47e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Mon, 4 May 2026 19:03:08 -0700 Subject: [PATCH 6/7] Add a JMH benchmark for the version check --- core/build.gradle.kts | 6 +- .../smithy/java/core/VersionCheckBench.java | 59 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 core/src/jmh/java/software/amazon/smithy/java/core/VersionCheckBench.java diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 56c859235..c118c44fa 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -17,7 +17,11 @@ dependencies { implementation(project(":logging")) } -jmh {} +jmh { + includes.addAll(providers.gradleProperty("jmh.includes") + .map { listOf(it) } + .orElse(emptyList())) +} // Run all tests with a different locale to ensure we are not doing anything locale specific. val localeTest = diff --git a/core/src/jmh/java/software/amazon/smithy/java/core/VersionCheckBench.java b/core/src/jmh/java/software/amazon/smithy/java/core/VersionCheckBench.java new file mode 100644 index 000000000..059d3ebb7 --- /dev/null +++ b/core/src/jmh/java/software/amazon/smithy/java/core/VersionCheckBench.java @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.core; + +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Measures the startup cost of the version compatibility check. + * + *

In production, {@code VersionCheck.check()} runs exactly once during class + * initialization. This benchmark measures the per-invocation cost to quantify + * the one-time startup impact. + */ +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(1) +public class VersionCheckBench { + + private static final String VERSION = Version.VERSION; + + @Setup + public void setup() { + Logger.getLogger(VersionCheck.class.getName()).setLevel(Level.OFF); + } + + @Benchmark + public void versionCheckEnabled() { + VersionCheck.check(VERSION); + } + + @Benchmark + public void versionCheckSkipped() { + System.setProperty("smithy.java.skipVersionCheck", "true"); + try { + VersionCheck.check(VERSION); + } finally { + System.clearProperty("smithy.java.skipVersionCheck"); + } + } +} From 55008549d0c618cd8576f6c41935dbb84ddacd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Mon, 4 May 2026 19:18:40 -0700 Subject: [PATCH 7/7] Suppress a spotBugs warning in the benchmark --- core/build.gradle.kts | 9 ++++++--- .../amazon/smithy/java/core/VersionCheckBench.java | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index c118c44fa..7eb24ef74 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -18,9 +18,12 @@ dependencies { } jmh { - includes.addAll(providers.gradleProperty("jmh.includes") - .map { listOf(it) } - .orElse(emptyList())) + includes.addAll( + providers + .gradleProperty("jmh.includes") + .map { listOf(it) } + .orElse(emptyList()), + ) } // Run all tests with a different locale to ensure we are not doing anything locale specific. diff --git a/core/src/jmh/java/software/amazon/smithy/java/core/VersionCheckBench.java b/core/src/jmh/java/software/amazon/smithy/java/core/VersionCheckBench.java index 059d3ebb7..3e34b98a9 100644 --- a/core/src/jmh/java/software/amazon/smithy/java/core/VersionCheckBench.java +++ b/core/src/jmh/java/software/amazon/smithy/java/core/VersionCheckBench.java @@ -5,6 +5,7 @@ package software.amazon.smithy.java.core; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -17,7 +18,6 @@ import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.TearDown; import org.openjdk.jmh.annotations.Warmup; /** @@ -38,6 +38,7 @@ public class VersionCheckBench { private static final String VERSION = Version.VERSION; @Setup + @SuppressFBWarnings(value = "LG_LOST_LOGGER_DUE_TO_WEAK_REFERENCE", justification = "Intentional for benchmark") public void setup() { Logger.getLogger(VersionCheck.class.getName()).setLevel(Level.OFF); }