Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plugin is "published" from smithy-swift so that it can be installed into service clients that smithy-swift creates.

See awslabs/aws-sdk-swift#2058 to see how a Smithy-based SDK installs the plugin into a service client target.

],
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"),
]
Expand Down Expand Up @@ -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"),
]
),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SmithyCodeGenerator is the Swift build plugin that we add to service clients to perform their codegen.

SmithyCodegenCLI is a command-line tool that the build plugin invokes to actually generate the code. (Eventually this code generation tool will be usable from the CLI but for now, it's solely for the use of the build plugin.)

.testTarget(
name: "ClientRuntimeTests",
dependencies: [
Expand Down
73 changes: 73 additions & 0 deletions Plugins/SmithyCodeGenerator/SmithyCodeGeneratorPlugin.swift
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor Author

@jbelkins jbelkins Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the build tool plugin implementation.

  • Reads the smithy-model-info.json file to get the model location in the form of a file URL. (All other source files in the target are ignored.)
  • Constructs an output file URL for code-generated schemas. This file will be written to the plugin's "working directory" which is provided by the Swift build system.
  • Constructs a build command that uses the SmithyCodegenCLI tool to read the model & generate a schemas file from it.

Note that the plugin doesn't actually invoke the code generation tool. Rather, the Swift build system will insert the invocation into the build process and will perform it at the appropriate time.


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
}
65 changes: 65 additions & 0 deletions Sources/SmithyCodegenCLI/SmithyCodegenCLI.swift
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor Author

@jbelkins jbelkins Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the CLI tool that will be invoked to perform code generation.

This CLI tool is currently just a shell - it resolves file URLs for its parameters, then just creates an empty schemas Swift file as its output (the file must exist, even if empty, for the build to succeed, since the build plugin declares a schemas Swift file as its output). Actually implementing code generation will be a follow-on task.


@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
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ class DirectedSwiftCodegen(
DependencyJSONGenerator(ctx).writePackageJSON(writers.dependencies)
}

LOGGER.info("Generating Smithy model file info")
SmithyModelFileInfoGenerator(ctx).writeSmithyModelFileInfo()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invokes the model file info generator (see generator implementation immediately below.)


LOGGER.info("Flushing swift writers")
writers.flushWriters()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file generates a file named smithy-model-info.json into a service client target.

Its contents are

{"path":"relative/path/to/model.json"}

where relative/path/to/model.json is the path to the JSON AST file for this service, relative to project root.

The Swift code generator will use this file to locate the model at build time.

class SmithyModelFileInfoGenerator(
val ctx: ProtocolGenerator.GenerationContext,
) {
fun writeSmithyModelFileInfo() {
ctx.service.getTrait<ServiceTrait>()?.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\"}")
}
}
}
}
Loading