diff --git a/Package.swift b/Package.swift index 940b1e6bf..03e338843 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,23 @@ let package = Package( .target( name: "SmithyWaitersAPI" ), + .plugin( + name: "SmithyCodeGenerator", + capability: .buildTool(), + dependencies: [ + "SmithyCodegenCLI", + ] + ), + .executableTarget( + name: "SmithyCodegenCLI", + dependencies: [ + "SmithyCodegenCore", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), + .target( + name: "SmithyCodegenCore" + ), .testTarget( name: "ClientRuntimeTests", dependencies: [ diff --git a/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift new file mode 100644 index 000000000..d3e1bede5 --- /dev/null +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -0,0 +1,66 @@ +// +// 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(for: $0, in: context.pluginWorkDirectory, with: smithyCodegenCLITool.path) + } + } + + private func createBuildCommand( + for inputPath: Path, + in outputDirectoryPath: Path, + with generatorToolPath: Path + ) throws -> Command? { + let currentWorkingDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + + // Skip any file that isn't the smithy-model-info.json for this service. + guard inputPath.lastComponent == "smithy-model-info.json" else { return nil } + + // 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 = currentWorkingDirectoryURL.appendingPathComponent(smithyModelInfo.path) + let modelPath = Path(modelPathURL.path) + + // Return a command that will run during the build to generate the output file. + let modelCountSwiftPath = outputDirectoryPath.appending("ModelCount.swift") + return .buildCommand( + displayName: "Generating Swift source files from \(smithyModelInfo.path)", + executable: generatorToolPath, + arguments: [modelPath, modelCountSwiftPath], + inputFiles: [inputPath, modelPath], + outputFiles: [modelCountSwiftPath] + ) + } +} + +/// 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 +} + +struct Err: Error { + var localizedDescription: String { "boom" } +} diff --git a/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift new file mode 100644 index 000000000..4aa65a65d --- /dev/null +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -0,0 +1,51 @@ +// +// 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 path to the JSON model file.") + var modelPath: String + + @Argument(help: "The full path to write the output file. Intermediate directories will be created as needed.") + var outputPath: String + + func run() async throws { + guard FileManager.default.fileExists(atPath: modelPath) else { + throw SmithySchemaCodegenToolError(localizedDescription: "no file at model path") + } + let model = try SmithyModel(modelFileURL: URL(fileURLWithPath: modelPath)) + + let outputFileURL = URL(fileURLWithPath: outputPath) + let contents = """ + // + // Copyright Amazon.com Inc. or its affiliates. + // All Rights Reserved. + // + // SPDX-License-Identifier: Apache-2.0 + // + + // Code generated by SmithyCodegenCLI. DO NOT EDIT! + + /// The count of bytes in the model. + public let modelCount = \(model.count) + """ + try FileManager.default.createDirectory( + at: outputFileURL.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + try Data(contents.utf8).write(to: outputFileURL) + } +} + +struct SmithySchemaCodegenToolError: Error { + let localizedDescription: String +} diff --git a/Sources/SmithyCodegenCore/SmithyModel.swift b/Sources/SmithyCodegenCore/SmithyModel.swift new file mode 100644 index 000000000..f5dbc7b4a --- /dev/null +++ b/Sources/SmithyCodegenCore/SmithyModel.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import struct Foundation.URL + +public struct SmithyModel { + public let count: Int + + public init(modelFileURL: URL) throws { + let modelData = try Data(contentsOf: modelFileURL) + self.count = modelData.count + } +} 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\"}") + } + } + } +}