From 65e7c3b65b978d353e66386911fe27a2c2307a0e Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 15 Sep 2026 16:12:27 +0200 Subject: [PATCH 1/2] fix quick startup guide --- Examples/HelloWorldNoTraits/README.md | 2 +- .../lambda-init/Initializer.swift | 56 +++++++++++++++++++ .../Resources/code/03-02-03-package.swift | 2 +- .../Resources/code/03-02-04-package.swift | 2 +- .../Resources/code/03-02-05-package.swift | 2 +- .../Docs.docc/lambda-handlers.md | 2 +- .../Docs.docc/managed-instances.md | 2 +- .../AWSLambdaRuntime/Docs.docc/quick-setup.md | 2 +- readme.md | 4 +- 9 files changed, 65 insertions(+), 9 deletions(-) diff --git a/Examples/HelloWorldNoTraits/README.md b/Examples/HelloWorldNoTraits/README.md index 0b90ed51..6aeb7579 100644 --- a/Examples/HelloWorldNoTraits/README.md +++ b/Examples/HelloWorldNoTraits/README.md @@ -26,7 +26,7 @@ This example disables all the traits. To disable one or several traits, modify ` ```swift dependencies: [ - .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0-beta", traits: []) + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "3.0.0", traits: []) ], ``` diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift index fabec9e1..153dbf64 100644 --- a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift @@ -18,6 +18,9 @@ import FoundationEssentials import Foundation #endif +/// The `SupportedPlatform.MacOSVersion` matching the `LambdaSwift 2.0` availability macro. +private let minimumMacOSVersion = "v15" + @available(LambdaSwift 2.0, *) struct Initializer { @@ -59,6 +62,14 @@ struct Initializer { with: "" ) print("✅ Lambda function written to \(relativePath)") + + // The runtime API is only available on macOS 15 or later. Without the matching + // platform requirement, the generated function does not compile on macOS. + try self.addMacOSPlatformRequirement( + to: configuration.destinationDir, + verboseLogging: configuration.verboseLogging + ) + print("📦 You can now package with: 'swift package lambda-build'") } catch { print("🛑 Failed to create the Lambda function file: \(error)") @@ -68,6 +79,51 @@ struct Initializer { } } + /// Adds the macOS platform requirement of `AWSLambdaRuntime` to the package manifest. + /// + /// The runtime API is annotated with `@available(LambdaSwift 2.0, *)`, an availability macro + /// that expands to `macOS 15.0`. A package that does not declare that platform requirement + /// fails to build on macOS with "is only available in macOS 15.0 or newer". + /// + /// `platforms` is set by appending an assignment at the end of the manifest rather than by + /// inserting an argument into the `Package(...)` call. `Package` is a class, so mutating it + /// after initialization is valid, and appending needs no parsing of the existing manifest. + private func addMacOSPlatformRequirement(to destinationDir: URL, verboseLogging: Bool) throws { + let manifestURL = destinationDir.appendingPathComponent("Package.swift") + + guard FileManager.default.fileExists(atPath: manifestURL.path) else { + print("⚠️ No Package.swift found at \(manifestURL.path).") + print(" Add 'platforms: [.macOS(.\(minimumMacOSVersion))]' to your package manifest manually.") + return + } + + let manifest = try String(contentsOf: manifestURL, encoding: .utf8) + + // Don't override platform requirements the developer already declared. + let declaresPlatforms = manifest.split(separator: "\n").contains { line in + let statement = line.trimming(while: \.isWhitespace) + return statement.hasPrefix("platforms:") || statement.hasPrefix("package.platforms") + } + guard !declaresPlatforms else { + if verboseLogging { + print("Package.swift already declares platform requirements, leaving it unchanged") + } + return + } + + let existing = manifest.hasSuffix("\n") ? manifest : manifest + "\n" + try """ + \(existing) + // The AWSLambdaRuntime API requires macOS 15 or later. + package.platforms = [ + .macOS(.\(minimumMacOSVersion)) + ] + + """.write(to: manifestURL, atomically: true, encoding: .utf8) + + print("✅ Added 'package.platforms = [.macOS(.\(minimumMacOSVersion))]' to Package.swift") + } + /// Finds the main entry point Swift file in the Sources directory. /// /// Strategy: diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift index ca7d67f2..897e8fd5 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift @@ -9,6 +9,6 @@ let package = Package( .macOS(.v15) ], dependencies: [ - .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0") + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "3.0.0") ] ) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift index 1cc49845..a2d03562 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift @@ -12,6 +12,6 @@ let package = Package( .executable(name: "PalindromeLambda", targets: ["PalindromeLambda"]) ], dependencies: [ - .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0") + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "3.0.0") ] ) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift index 0be7e68e..638adc7d 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift @@ -12,7 +12,7 @@ let package = Package( .executable(name: "PalindromeLambda", targets: ["PalindromeLambda"]) ], dependencies: [ - .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0") + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "3.0.0") ], targets: [ .executableTarget( diff --git a/Sources/AWSLambdaRuntime/Docs.docc/lambda-handlers.md b/Sources/AWSLambdaRuntime/Docs.docc/lambda-handlers.md index c7e3fc5a..41106a8e 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/lambda-handlers.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/lambda-handlers.md @@ -67,7 +67,7 @@ let package = Package( name: "MyLambda", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"), + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "3.0.0"), .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/managed-instances.md b/Sources/AWSLambdaRuntime/Docs.docc/managed-instances.md index d9d1bbb9..b10c13d1 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/managed-instances.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/managed-instances.md @@ -91,7 +91,7 @@ The managed instances support is implemented behind a Swift package trait (`Mana dependencies: [ .package( url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", - from: "2.0.0", + from: "3.0.0", traits: [ // Keep other default traits but exclude ManagedRuntimeSupport "FoundationJSONSupport", diff --git a/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md index 2a0a98ae..0039811e 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md @@ -44,7 +44,7 @@ let package = Package( .executable(name: "MyFirstLambdaFunction", targets: ["MyFirstLambdaFunction"]), ], dependencies: [ - .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"), + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "3.0.0"), ], targets: [ .executableTarget( diff --git a/readme.md b/readme.md index cdcf7256..ecdfaa91 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ You need the Swift 6.x toolchain, [Docker](https://docs.docker.com/desktop/insta ```bash mkdir MyLambda && cd MyLambda swift package init --type executable --name MyLambda -swift package add-dependency https://github.com/awslabs/swift-aws-lambda-runtime.git --from 2.0.0 +swift package add-dependency https://github.com/awslabs/swift-aws-lambda-runtime.git --from 3.0.0 swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime ``` @@ -93,7 +93,7 @@ aws lambda invoke \ /dev/stdout ``` -For the full walkthrough, see the [getting started guide](https://swiftpackageindex.com/awslabs/swift-aws-lambda-runtime/documentation/awslambdaruntime/quick-setup) and the [tutorial](https://swiftpackageindex.com/awslabs/swift-aws-lambda-runtime/documentation/awslambdaruntime/tutorials/table-of-content). +For the full walkthrough, see the [getting started guide](https://swiftpackageindex.com/awslabs/swift-aws-lambda-runtime/documentation/awslambdaruntime/quick-setup) and the [tutorial](https://swiftpackageindex.com/awslabs/swift-aws-lambda-runtime/main/tutorials/table-of-content). ## Examples From edb05890a0d3006bad8bcf8a4d0bd4b94ccc6dbd Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 15 Sep 2026 16:32:55 +0200 Subject: [PATCH 2/2] test: add a suite to exercise lambda-init --- .../InitializerTests.swift | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 Tests/AWSLambdaPluginHelperTests/InitializerTests.swift diff --git a/Tests/AWSLambdaPluginHelperTests/InitializerTests.swift b/Tests/AWSLambdaPluginHelperTests/InitializerTests.swift new file mode 100644 index 00000000..8ce3d9f2 --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/InitializerTests.swift @@ -0,0 +1,320 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import AWSLambdaPluginHelper + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// Tests for the `lambda-init` plugin. +/// +/// Every test drives the real `Initializer.initialize(arguments:)` entry point against a throwaway +/// package directory on disk and asserts on the resulting files, so the tests cover the plugin as +/// a whole rather than its internals. +@Suite("lambda-init plugin") +struct InitializerTests { + + // MARK: - The generated function + + @available(LambdaSwift 2.0, *) + @Test("Writes the JSON template over the existing entry point") + func writesJSONTemplate() async throws { + try await withTemporaryPackage(sources: ["MyLambda/MyLambda.swift": "print(\"placeholder\")\n"]) { package in + try await runLambdaInit(in: package) + + let entryPoint = try contents(of: package, at: "Sources/MyLambda/MyLambda.swift") + #expect(entryPoint == functionWithJSONTemplate) + } + } + + @available(LambdaSwift 2.0, *) + @Test("--with-url writes the FunctionURL template") + func writesFunctionURLTemplate() async throws { + try await withTemporaryPackage(sources: ["MyLambda/MyLambda.swift": "print(\"placeholder\")\n"]) { package in + try await runLambdaInit(in: package, arguments: ["--with-url"]) + + let entryPoint = try contents(of: package, at: "Sources/MyLambda/MyLambda.swift") + #expect(entryPoint == functionWithUrlTemplate) + } + } + + @available(LambdaSwift 2.0, *) + @Test("Backs up the entry point it overwrites") + func backsUpTheEntryPoint() async throws { + let original = "print(\"placeholder\")\n" + try await withTemporaryPackage(sources: ["MyLambda/MyLambda.swift": original]) { package in + try await runLambdaInit(in: package) + + let backup = try contents(of: package, at: "Sources/MyLambda/MyLambda.swift.bak") + #expect(backup == original) + } + } + + // MARK: - Entry point discovery + + @available(LambdaSwift 2.0, *) + @Test("Prefers main.swift inside the target directory and leaves other files alone") + func prefersMainSwiftInTargetDirectory() async throws { + let sources = [ + "MyLambda/main.swift": "print(\"placeholder\")\n", + "MyLambda/Helper.swift": "let helper = 1\n", + ] + try await withTemporaryPackage(sources: sources) { package in + try await runLambdaInit(in: package) + + #expect(try contents(of: package, at: "Sources/MyLambda/main.swift") == functionWithJSONTemplate) + #expect(try contents(of: package, at: "Sources/MyLambda/Helper.swift") == "let helper = 1\n") + } + } + + @available(LambdaSwift 2.0, *) + @Test("Uses the file declaring @main") + func usesTheFileDeclaringMain() async throws { + let sources = [ + "MyLambda/Entry.swift": "@main\nstruct Entry {\n static func main() {}\n}\n", + "MyLambda/Helper.swift": "let helper = 1\n", + ] + try await withTemporaryPackage(sources: sources) { package in + try await runLambdaInit(in: package) + + #expect(try contents(of: package, at: "Sources/MyLambda/Entry.swift") == functionWithJSONTemplate) + #expect(try contents(of: package, at: "Sources/MyLambda/Helper.swift") == "let helper = 1\n") + } + } + + @available(LambdaSwift 2.0, *) + @Test("Creates .swift when the target directory has no recognizable entry point") + func createsNamedFileWhenNoEntryPointExists() async throws { + try await withTemporaryPackage(sources: ["MyLambda/Helper.swift": "let helper = 1\n"]) { package in + try await runLambdaInit(in: package) + + #expect(try contents(of: package, at: "Sources/MyLambda/MyLambda.swift") == functionWithJSONTemplate) + } + } + + @available(LambdaSwift 2.0, *) + @Test("Handles Sources/main.swift without a target subdirectory") + func handlesFlatSourcesLayout() async throws { + try await withTemporaryPackage(sources: ["main.swift": "print(\"placeholder\")\n"]) { package in + try await runLambdaInit(in: package) + + #expect(try contents(of: package, at: "Sources/main.swift") == functionWithJSONTemplate) + } + } + + @available(LambdaSwift 2.0, *) + @Test("Falls back to Sources/main.swift when several targets are present") + func fallsBackToSourcesMainSwiftWithSeveralTargets() async throws { + let sources = [ + "TargetA/a.swift": "let a = 1\n", + "TargetB/b.swift": "let b = 2\n", + ] + try await withTemporaryPackage(sources: sources) { package in + try await runLambdaInit(in: package) + + #expect(try contents(of: package, at: "Sources/main.swift") == functionWithJSONTemplate) + #expect(try contents(of: package, at: "Sources/TargetA/a.swift") == "let a = 1\n") + #expect(try contents(of: package, at: "Sources/TargetB/b.swift") == "let b = 2\n") + } + } + + // MARK: - The macOS platform requirement + + @available(LambdaSwift 2.0, *) + @Test("Appends the macOS platform requirement to the manifest") + func appendsPlatformRequirement() async throws { + try await withTemporaryPackage { package in + try await runLambdaInit(in: package) + + let manifest = try contents(of: package, at: "Package.swift") + #expect(manifest == Self.manifestWithoutPlatforms + "\n" + Self.platformRequirement + "\n") + } + } + + @available(LambdaSwift 2.0, *) + @Test("Appends the platform requirement when the manifest has no trailing newline") + func appendsPlatformRequirementWithoutTrailingNewline() async throws { + let source = String(Self.manifestWithoutPlatforms.dropLast()) + try await withTemporaryPackage(manifest: source) { package in + try await runLambdaInit(in: package) + + let manifest = try contents(of: package, at: "Package.swift") + // The assignment must land on its own line, not glued to the closing parenthesis. + #expect(manifest == source + "\n\n" + Self.platformRequirement + "\n") + } + } + + @available(LambdaSwift 2.0, *) + @Test("Running twice does not duplicate the platform requirement") + func platformRequirementIsIdempotent() async throws { + try await withTemporaryPackage { package in + try await runLambdaInit(in: package) + let afterFirstRun = try contents(of: package, at: "Package.swift") + + try await runLambdaInit(in: package) + let afterSecondRun = try contents(of: package, at: "Package.swift") + + #expect(afterSecondRun == afterFirstRun) + } + } + + @available(LambdaSwift 2.0, *) + @Test("Leaves a manifest that already declares platforms untouched") + func leavesExistingPlatformsDeclarationUntouched() async throws { + let source = Self.manifestWithoutPlatforms.replacing( + " name: \"MyLambda\",\n", + with: " name: \"MyLambda\",\n platforms: [.macOS(.v15)],\n" + ) + try await withTemporaryPackage(manifest: source) { package in + try await runLambdaInit(in: package) + + #expect(try contents(of: package, at: "Package.swift") == source) + } + } + + @available(LambdaSwift 2.0, *) + @Test("Leaves an existing package.platforms assignment untouched") + func leavesExistingAssignmentUntouched() async throws { + let source = Self.manifestWithoutPlatforms + "\npackage.platforms = [.macOS(.v26)]\n" + try await withTemporaryPackage(manifest: source) { package in + try await runLambdaInit(in: package) + + #expect(try contents(of: package, at: "Package.swift") == source) + } + } + + @available(LambdaSwift 2.0, *) + @Test("Still writes the function when Package.swift is missing") + func toleratesMissingManifest() async throws { + try await withTemporaryPackage( + manifest: nil, + sources: ["MyLambda/MyLambda.swift": "print(\"placeholder\")\n"] + ) { package in + try await runLambdaInit(in: package) + + #expect(try contents(of: package, at: "Sources/MyLambda/MyLambda.swift") == functionWithJSONTemplate) + #expect(!FileManager.default.fileExists(atPath: package.appending(path: "Package.swift").path)) + } + } + + // MARK: - Arguments + + @available(LambdaSwift 2.0, *) + @Test("--help leaves the package untouched") + func helpLeavesThePackageUntouched() async throws { + let entryPoint = "print(\"placeholder\")\n" + try await withTemporaryPackage(sources: ["MyLambda/MyLambda.swift": entryPoint]) { package in + try await runLambdaInit(in: package, arguments: ["--help"]) + + #expect(try contents(of: package, at: "Package.swift") == Self.manifestWithoutPlatforms) + #expect(try contents(of: package, at: "Sources/MyLambda/MyLambda.swift") == entryPoint) + } + } + + @available(LambdaSwift 2.0, *) + @Test("--verbose does not change the outcome") + func verboseDoesNotChangeTheOutcome() async throws { + try await withTemporaryPackage(sources: ["MyLambda/MyLambda.swift": "print(\"placeholder\")\n"]) { package in + try await runLambdaInit(in: package, arguments: ["--verbose"]) + + #expect(try contents(of: package, at: "Sources/MyLambda/MyLambda.swift") == functionWithJSONTemplate) + #expect(try contents(of: package, at: "Package.swift").hasSuffix(Self.platformRequirement + "\n")) + } + } + + // MARK: - Fixtures + + /// What `swift package init --type executable --name MyLambda` produces, plus the runtime + /// dependency added by `swift package add-dependency`. It declares no platform requirement. + private static let manifestWithoutPlatforms = """ + // swift-tools-version: 6.2 + import PackageDescription + + let package = Package( + name: "MyLambda", + dependencies: [ + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "3.0.0") + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ] + ) + ] + ) + + """ + + /// The block the plugin is expected to append. Spelled out here on purpose: the test has to + /// fail when the generated manifest changes shape. + private static let platformRequirement = """ + // The AWSLambdaRuntime API requires macOS 15 or later. + package.platforms = [ + .macOS(.v15) + ] + """ + + // MARK: - Helpers + + /// Creates a package directory in a unique temporary location, runs `body`, then deletes it. + /// + /// - Parameters: + /// - manifest: The content of `Package.swift`, or `nil` to leave the manifest out. + /// - sources: File contents keyed by their path relative to `Sources/`. Pass an empty + /// dictionary to leave the `Sources` directory out. + @available(LambdaSwift 2.0, *) + private func withTemporaryPackage( + manifest: String? = Self.manifestWithoutPlatforms, + sources: [String: String] = ["MyLambda/MyLambda.swift": "print(\"placeholder\")\n"], + _ body: (URL) async throws -> Void + ) async throws { + let package = FileManager.default.temporaryDirectory.appending(path: "lambda-init-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: package, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: package) } + + if let manifest { + try manifest.write(to: package.appending(path: "Package.swift"), atomically: true, encoding: .utf8) + } + + for (relativePath, content) in sources { + let file = package.appending(path: "Sources").appending(path: relativePath) + try FileManager.default.createDirectory( + at: file.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + try content.write(to: file, atomically: true, encoding: .utf8) + } + + try await body(package) + } + + /// Runs the plugin against `package`, the way the SwiftPM plugin invokes the helper. + @available(LambdaSwift 2.0, *) + private func runLambdaInit(in package: URL, arguments: [String] = []) async throws { + try await Initializer().initialize(arguments: ["--dest-dir", package.path] + arguments) + } + + /// The content of the file at `relativePath` inside `package`. + private func contents(of package: URL, at relativePath: String) throws -> String { + try String(contentsOf: package.appending(path: relativePath), encoding: .utf8) + } +}