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..180e3bb5e --- /dev/null +++ b/Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift @@ -0,0 +1,73 @@ +// +// 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..b8b247155 --- /dev/null +++ b/Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift @@ -0,0 +1,65 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ArgumentParser +import Foundation + +@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) + + // 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 { + // 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\"}") + } + } + } +}