From 21ed6f74ca48307181121e04ee9996a9254458aa Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 11:58:30 -0600 Subject: [PATCH 1/5] feat: Create code generator plugin --- Package.swift | 15 ++++ .../SmithyCodeGeneratorPlugin.swift | 68 +++++++++++++++++++ .../SmithyCodegenCLI/SmithyCodegenCLI.swift | 62 +++++++++++++++++ .../swift/codegen/DirectedSwiftCodegen.kt | 3 + .../codegen/SmithyModelFileInfoGenerator.kt | 25 +++++++ 5 files changed, 173 insertions(+) create mode 100644 Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift create mode 100644 Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt diff --git a/Package.swift b/Package.swift index 940b1e6bf..c0dfb0a98 100644 --- a/Package.swift +++ b/Package.swift @@ -53,10 +53,12 @@ let package = Package( .library(name: "SmithyCBOR", targets: ["SmithyCBOR"]), .library(name: "SmithyWaitersAPI", targets: ["SmithyWaitersAPI"]), .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]), + .plugin(name: "SmithyCodeGenerator", targets: ["SmithyCodeGenerator"]), ], dependencies: { var dependencies: [Package.Dependency] = [ .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.54.2"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.13.0"), ] @@ -258,6 +260,19 @@ let package = Package( .target( name: "SmithyWaitersAPI" ), + .plugin( + name: "SmithyCodeGenerator", + capability: .buildTool(), + dependencies: [ + "SmithyCodegenCLI", + ] + ), + .executableTarget( + name: "SmithyCodegenCLI", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), .testTarget( name: "ClientRuntimeTests", dependencies: [ diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift new file mode 100644 index 000000000..116d4b9d0 --- /dev/null +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -0,0 +1,68 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import class Foundation.FileManager +import class Foundation.JSONDecoder +import struct Foundation.URL +import PackagePlugin + +@main +struct SmithyCodeGeneratorPlugin: BuildToolPlugin { + + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + // This plugin only runs for package targets that can have source files. + guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] } + + // Retrieve the `SmithyCodegenCLI` tool from the plugin's tools. + let smithyCodegenCLITool = try context.tool(named: "SmithyCodegenCLI") + + // Construct a build command for each source file with a particular suffix. + return try sourceFiles.map(\.path).compactMap { + try createBuildCommand(name: target.name, for: $0, in: context.pluginWorkDirectory, with: smithyCodegenCLITool.path) + } + } + + private func createBuildCommand( + name: String, + for inputPath: Path, + in outputDirectoryPath: Path, + with generatorToolPath: Path + ) throws -> Command? { + // Skip any file that isn't the smithy-model-info.json for this service. + guard inputPath.lastComponent == "smithy-model-info.json" else { return nil } + + let currentWorkingDirectoryFileURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + + // Get the smithy model path. + let modelInfoData = try Data(contentsOf: URL(fileURLWithPath: inputPath.string)) + let smithyModelInfo = try JSONDecoder().decode(SmithyModelInfo.self, from: modelInfoData) + let modelPathURL = currentWorkingDirectoryFileURL.appendingPathComponent(smithyModelInfo.path) + let modelPath = Path(modelPathURL.path) + + // Construct the schemas.swift path. + let schemasSwiftPath = outputDirectoryPath.appending("\(name)Schemas.swift") + + // Construct the build command that invokes SmithyCodegenCLI. + return .buildCommand( + displayName: "Generating Swift source files from model file \(smithyModelInfo.path)", + executable: generatorToolPath, + arguments: [ + "--schemas-path", schemasSwiftPath, + modelPath + ], + inputFiles: [inputPath, modelPath], + outputFiles: [schemasSwiftPath] + ) + } +} + +/// Codable structure for reading the contents of `smithy-model-info.json` +private struct SmithyModelInfo: Decodable { + /// The path to the model, from the root of the target's project. Required. + let path: String +} diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift new file mode 100644 index 000000000..bf8598d60 --- /dev/null +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -0,0 +1,62 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ArgumentParser +import Foundation +import SmithyCodegenCore + +@main +struct SmithyCodegenCLI: AsyncParsableCommand { + + @Argument(help: "The full or relative path to the JSON model file.") + var modelPath: String + + @Option(help: "The full or relative path to write the schemas output file.") + var schemasPath: String? + + func run() async throws { + let currentWorkingDirectoryFileURL = currentWorkingDirectoryFileURL() + print("Current working directory: \(currentWorkingDirectoryFileURL.path)") + + // Create the model file URL + let modelFileURL = URL(fileURLWithPath: modelPath, relativeTo: currentWorkingDirectoryFileURL) + guard FileManager.default.fileExists(atPath: modelFileURL.path) else { + throw SmithyCodegenCLIError(localizedDescription: "no file at model path \(modelFileURL.path)") + } + print("Model file path: \(modelFileURL.path)") + + // If --schemas-path was supplied, create the schema file URL + let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) + + // Use resolved file URLs to run code generator + try CodeGenerator(modelFileURL: modelFileURL, schemasFileURL: schemasFileURL).run() + } + + private func currentWorkingDirectoryFileURL() -> URL { + // Get the current working directory as a file URL + var currentWorkingDirectoryPath = FileManager.default.currentDirectoryPath + if !currentWorkingDirectoryPath.hasSuffix("/") { + currentWorkingDirectoryPath.append("/") + } + return URL(fileURLWithPath: currentWorkingDirectoryPath) + } + + private func resolve(paramName: String, path: String?) -> URL? { + if let path { + let fileURL = URL(fileURLWithPath: path, relativeTo: currentWorkingDirectoryFileURL()) + print("Resolved \(paramName): \(fileURL.path)") + return fileURL + } else { + print("\(paramName) not provided, skipping generation") + return nil + } + } +} + +struct SmithyCodegenCLIError: Error { + let localizedDescription: String +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt index 94a56b8bc..154348e74 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/DirectedSwiftCodegen.kt @@ -107,6 +107,9 @@ class DirectedSwiftCodegen( DependencyJSONGenerator(ctx).writePackageJSON(writers.dependencies) } + LOGGER.info("Generating Smithy model file info") + SmithyModelFileInfoGenerator(ctx).writeSmithyModelFileInfo() + LOGGER.info("Flushing swift writers") writers.flushWriters() } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt new file mode 100644 index 000000000..0662d8d7d --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyModelFileInfoGenerator.kt @@ -0,0 +1,25 @@ +package software.amazon.smithy.swift.codegen + +import software.amazon.smithy.aws.traits.ServiceTrait +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.model.getTrait + +class SmithyModelFileInfoGenerator( + val ctx: ProtocolGenerator.GenerationContext, +) { + fun writeSmithyModelFileInfo() { + ctx.service.getTrait()?.let { serviceTrait -> + val filename = "Sources/${ctx.settings.moduleName}/smithy-model-info.json" + val modelFileName = + serviceTrait + .sdkId + .lowercase() + .replace(",", "") + .replace(" ", "-") + val contents = "codegen/sdk-codegen/aws-models/$modelFileName.json" + ctx.delegator.useFileWriter(filename) { writer -> + writer.write("{\"path\":\"$contents\"}") + } + } + } +} From 4ddfc98eff019e8a6e873b5f60fd40126971b16d Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 12:03:30 -0600 Subject: [PATCH 2/5] Fix swiftlint --- .../SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift index 116d4b9d0..180e3bb5e 100644 --- a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -23,7 +23,12 @@ struct SmithyCodeGeneratorPlugin: BuildToolPlugin { // Construct a build command for each source file with a particular suffix. return try sourceFiles.map(\.path).compactMap { - try createBuildCommand(name: target.name, for: $0, in: context.pluginWorkDirectory, with: smithyCodegenCLITool.path) + try createBuildCommand( + name: target.name, + for: $0, + in: context.pluginWorkDirectory, + with: smithyCodegenCLITool.path + ) } } From c57b182dedc3264bdb6befd0cc877e9d53aaf93f Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 12:08:32 -0600 Subject: [PATCH 3/5] Remove code generator reference --- Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index bf8598d60..d604f4e8b 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -31,9 +31,10 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) + print("Schemas file path: \(schemasFileURL?.path ?? "nil")") - // Use resolved file URLs to run code generator - try CodeGenerator(modelFileURL: modelFileURL, schemasFileURL: schemasFileURL).run() + // All file URLs needed for code generation have now been resolved. + // Implement code generation here. } private func currentWorkingDirectoryFileURL() -> URL { From 9bd4df2998800c8f48b10e73710cada2ee068456 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 12:43:30 -0600 Subject: [PATCH 4/5] Remove import --- Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index d604f4e8b..03da98c58 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -7,7 +7,6 @@ import ArgumentParser import Foundation -import SmithyCodegenCore @main struct SmithyCodegenCLI: AsyncParsableCommand { From f1ef4e852e295bd9a4d473d51e167d47942a8b0e Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Sat, 15 Nov 2025 12:51:34 -0600 Subject: [PATCH 5/5] Create empty Schemas swift file --- Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift index 03da98c58..b8b247155 100644 --- a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -30,10 +30,13 @@ struct SmithyCodegenCLI: AsyncParsableCommand { // If --schemas-path was supplied, create the schema file URL let schemasFileURL = resolve(paramName: "--schemas-path", path: schemasPath) - print("Schemas file path: \(schemasFileURL?.path ?? "nil")") // All file URLs needed for code generation have now been resolved. // Implement code generation here. + if let schemasFileURL { + print("Schemas file path: \(schemasFileURL)") + FileManager.default.createFile(atPath: schemasFileURL.path, contents: Data()) + } } private func currentWorkingDirectoryFileURL() -> URL {