Skip to content

Commit 0528feb

Browse files
sebstoSebastien Stormacqclaude
authored
v2 Plugin System: lambda-init, lambda-build, lambda-deploy (#567)
This PR delivers the v2 plugin system, replacing the legacy single-purpose `archive` plugin with three focused SwiftPM command plugins that cover the end-to-end developer experience: - **`lambda-init`** — Scaffold a new Lambda function from a template - **`lambda-build`** — Compile and package for Amazon Linux 2023 (via Docker or Apple container) - **`lambda-deploy`** — Deploy to AWS Lambda (create, update, or delete) The legacy `archive` command is preserved as a deprecated passthrough to `lambda-build`. ## Quick Start ```bash # Create a new project swift package init --type executable --name MyLambda # Scaffold a Lambda function swift package lambda-init --allow-writing-to-package-directory # Build for Amazon Linux swift package --allow-network-connections docker lambda-build # Deploy to AWS swift package --allow-network-connections all:443 lambda-deploy # Delete when done swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## Key Changes ### Architecture - All plugins are thin wrappers that spawn a shared `AWSLambdaPluginHelper` executable - The helper dispatches to `Initializer`, `Builder`, or `Deployer` based on `argv[1]` - AWS API calls use generated service clients (Lambda, IAM, S3, STS) built on SotoCore ### Builder (`lambda-build`) - Default base image changed from `amazonlinux2` to `amazonlinux2023` - AL2 blanket deprecation warning removed; targeted warning only when AL2 explicitly chosen - New `--cross-compile` option (replaces `--container-cli`): `docker`, `container`, `swift-static-sdk`, `custom-sdk` - Default binary stripping with `-Xlinker -s` and `--no-strip` opt-out - `--output-directory` accepted as deprecated alias for `--output-path` - Container CLI existence check with helpful install URLs ### Deployer (`lambda-deploy`) - Creates/updates/deletes Lambda functions with full IAM role lifecycle - Auto-creates IAM role with `AWSLambdaBasicExecutionRole` - S3 staging for archives > 50 MB - Function URL support with IAM auth (account-scoped, not world-accessible) - Auto-detects `FunctionURLRequest` usage in source code to enable URL without explicit `--with-url` - Ready-to-use invocation commands in deploy output ### Initializer (`lambda-init`) - Detects the actual entry point file (supports both `Sources/main.swift` and `Sources/<Name>/<Name>.swift`) - Backs up existing file before overwriting ### Dependencies - Added `soto-core` for AWS credential management, SigV4 signing, and HTTP transport - Removed vendored crypto/signer/HTTP code under `Vendored/` - Generated AWS service clients committed to the repository (maintainer-run generation script) ### Backward Compatibility - `swift package archive` preserved as deprecated alias (emits warning, delegates to `lambda-build`) - `--container-cli` accepted as deprecated alias for `--cross-compile` - `--output-directory` accepted as deprecated alias for `--output-path` - All original `archive` CLI options continue to work ## Documentation - Updated DocC articles, tutorials, and quick-setup guide - Updated all example READMEs with new plugin commands - SAM/CDK examples preserved with their respective deployment tools ## Known Limitations - Static Linux SDK CI uses `--build-system native`. On Swift 6.4 the new default build system (Swift Build) fails to statically link `AWSLambdaPluginHelper`: SotoCore transitively pulls in two vendored BoringSSL copies (swift-crypto's `CCryptoBoringSSL` and swift-nio-ssl's `CNIOBoringSSL`), and Swift Build emits duplicate C++ symbols at link time. The legacy build system links the same package cleanly, so the CI static build is pinned to `--build-system native` until the upstream issue is fixed: https://github.com/swiftlang/swift-build/issues/1485. - The `nightly-main` (6.5-dev) Linux job is expected to fail. The 6.5-dev compiler crashes while compiling `AWSLambdaRuntimeTests` (a SIL verification error in the `OwnershipModelEliminator` pass on a `Mutex` captured by a nested `Task` inside a task group). This is a toolchain bug, not a problem in this PR: the same code compiles on 6.1 through 6.4. Reported upstream with a minimal reproducer: swiftlang/swift#90211. `nightly-main` tracks an unreleased toolchain and is not a release target, so we are not working around it here. - SwiftPM prompts for network permission on first run (known [SPM issue swiftlang/swift#9763](swiftlang/swift-package-manager#9763)) - The shared documentation soundness check is temporarily disabled (`docs_check_enabled: false`). A regression in `swiftlang/github-workflows` (#282) passes the `.spi.yml` documentation target to `--target` with literal quotes, so the check fails with `no target named '"AWSLambdaRuntime"'`. Pinning the workflow version does not help because the reusable workflow always fetches its `check-docs.sh` from `main`. Re-enable once the upstream fix lands: swiftlang/github-workflows#290 (tracked in swiftlang/github-workflows#291). The SotoCore `LoginCredentialProvider` token-persistence issue in the SwiftPM sandbox is now resolved upstream ([soto-core#692](soto-project/soto-core#692), released in soto-core 7.14.0), and this PR already requires `from: "7.14.0"`. ## Testing - Unit tests for BuilderConfiguration and DeployerConfiguration argument parsing - Property-based tests for correctness properties (alias equivalence, cross-compile round-trip, mutual exclusion, bucket naming, archive threshold, AL2 warning logic) - End-to-end integration test script (`scripts/integration-test.sh`) - Manually tested: full create → invoke → update → delete lifecycle on AWS --------- Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 998c0f1 commit 0528feb

91 files changed

Lines changed: 7979 additions & 687 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/integration_tests.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ on:
3131
type: boolean
3232
description: "Boolean to enable the test of the archive plugin. Defaults to true."
3333
default: true
34+
archive_plugin_swift64_enabled:
35+
type: boolean
36+
description: "Boolean to enable the Swift 6.4 packaging plugin test (archive + lambda-build). Defaults to true."
37+
default: true
38+
archive_plugin_swift64_container_image:
39+
type: string
40+
description: "Container image providing the Swift 6.4+ toolchain for the packaging plugin test."
41+
default: "swiftlang/swift:nightly-6.4.x-bookworm"
3442
check_foundation_enabled:
3543
type: boolean
3644
description: "Boolean to enable the check for Foundation dependency. Defaults to true."
@@ -121,6 +129,69 @@ jobs:
121129
run: |
122130
.github/workflows/scripts/check-archive-plugin.sh
123131
132+
test-archive-plugin-swift64:
133+
name: Test packaging plugins (Swift 6.4)
134+
if: ${{ inputs.archive_plugin_swift64_enabled }}
135+
runs-on: ubuntu-latest
136+
strategy:
137+
fail-fast: false
138+
matrix:
139+
examples: ${{ fromJson(inputs.archive_plugin_examples) }}
140+
# Runs in the Swift 6.4 container so that `swift(>=6.4)` is true and the new
141+
# helper-delegating `archive` verb and the new `lambda-build` verb are compiled.
142+
#
143+
# The packaging plugins shell out to the HOST docker daemon (socket mounted below).
144+
# The plugin builds the example inside an Amazon Linux container and bind-mounts the
145+
# checkout into it. Because the daemon runs on the host, the bind-mount source must be
146+
# a HOST path: GitHub mounts the host work dir (/home/runner/work) at /__w inside this
147+
# job container, so a path like /__w/... does not exist for the host daemon. We mount
148+
# /home/runner/work at the SAME path here, and the test step runs the plugin from that
149+
# host-equivalent path, so the inner `docker run -v <src>:/workspace` source resolves
150+
# correctly on the host.
151+
container:
152+
image: ${{ inputs.archive_plugin_swift64_container_image }}
153+
volumes:
154+
- /var/run/docker.sock:/var/run/docker.sock
155+
- /home/runner/work:/home/runner/work
156+
steps:
157+
- name: Checkout repository
158+
uses: actions/checkout@v6
159+
with:
160+
persist-credentials: false
161+
162+
- name: Cache SPM dependencies
163+
uses: actions/cache@v5
164+
with:
165+
# Cache the whole .build, not just checkouts/repositories. The plugins
166+
# delegate to AWSLambdaPluginHelper, which links the SotoCore/NIO stack;
167+
# compiling that on the host takes ~220s per run. Caching .build lets
168+
# SwiftPM reuse the unchanged dependency objects and rebuild only the
169+
# sources that changed.
170+
path: Examples/${{ matrix.examples }}/.build
171+
key: spm-swift64-${{ runner.os }}-${{ matrix.examples }}-${{ hashFiles(format('Examples/{0}/Package.swift', matrix.examples)) }}
172+
restore-keys: |
173+
spm-swift64-${{ runner.os }}-${{ matrix.examples }}-
174+
175+
- name: Install dependencies
176+
run: |
177+
apt-get -qq update
178+
apt-get -qq -y install docker.io zip unzip file libssl-dev
179+
180+
- name: Test the packaging plugins
181+
env:
182+
EXAMPLE: ${{ matrix.examples }}
183+
run: |
184+
# GitHub mounts the host work dir (/home/runner/work) at /__w inside this
185+
# container, so $GITHUB_WORKSPACE is /__w/<repo>/<repo>. The plugin shells out
186+
# to the host docker daemon, which cannot resolve /__w paths. We mounted
187+
# /home/runner/work at the same path, so re-enter the checkout via the
188+
# host-equivalent path before running the plugin.
189+
# NOTE: this step runs under `sh` (dash) inside the container, so we use
190+
# POSIX `sed` rather than a bash-only `${var/a/b}` substitution.
191+
HOST_WORKSPACE=$(printf '%s\n' "$GITHUB_WORKSPACE" | sed 's#^/__w/#/home/runner/work/#')
192+
cd "${HOST_WORKSPACE}"
193+
.github/workflows/scripts/check-archive-plugin-swift64.sh
194+
124195
check-foundation:
125196
name: No dependencies on Foundation
126197
if: ${{ inputs.check_foundation_enabled }}

.github/workflows/pull_request.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,34 @@ jobs:
1919
docs_check_container_image: "swift:6.3-noble"
2020
format_check_container_image: "swift:6.3-noble"
2121
yamllint_check_enabled: true
22+
# Temporarily disabled: the shared docs check is broken upstream (it loads
23+
# check-docs.sh from main, where #282 passes the .spi.yml doc target to
24+
# `--target` JSON-quoted, failing with `no target named '"AWSLambdaRuntime"'`).
25+
# Pinning the workflow version does not help because the script is always fetched
26+
# from main. Re-enable once the upstream fix lands:
27+
# https://github.com/swiftlang/github-workflows/pull/290 (tracked in #291)
28+
docs_check_enabled: false
2229

2330
unit-tests:
2431
name: Unit tests
2532
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
2633
with:
2734
enable_windows_checks: false
28-
linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"6.0\"}]"
35+
# nightly-main (6.5-dev) is temporarily excluded: the compiler crashes with a SIL
36+
# verification error while compiling AWSLambdaRuntimeTests. This is a toolchain bug
37+
# (the same code compiles on 6.1-6.4), not an issue in this package. Remove the
38+
# nightly-main entry once fixed upstream: https://github.com/swiftlang/swift/issues/90211
39+
linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"6.0\"}, {\"swift_version\": \"nightly-main\"}]"
2940
swift_flags: "--explicit-target-dependency-import-check error"
3041
swift_nightly_flags: "--explicit-target-dependency-import-check error"
3142
enable_linux_static_sdk_build: true
43+
# On 6.4+ the default build system (Swift Build) fails to statically link AWSLambdaPluginHelper
44+
# against the static Linux SDK: SotoCore transitively pulls in two vendored BoringSSL forks
45+
# (swift-crypto's CCryptoBoringSSL and swift-nio-ssl's CNIOBoringSSL), and Swift Build emits
46+
# duplicate libc++abi symbols (set_new_handler, __cxa_*, ...) at link time. The legacy (native)
47+
# build system links the same package cleanly. Force it here until the upstream issue is fixed:
48+
# https://github.com/swiftlang/swift/issues/90196
49+
linux_static_sdk_build_command: "swift build --build-system native"
3250

3351
integration-tests:
3452
name: Integration Tests
@@ -43,6 +61,8 @@ jobs:
4361
examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'ManagedInstances', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming+APIGateway', 'Streaming+FunctionUrl', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]"
4462
archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]"
4563
archive_plugin_enabled: true
64+
archive_plugin_swift64_enabled: true
65+
archive_plugin_swift64_container_image: "swiftlang/swift:nightly-6.4.x-bookworm"
4666

4767
semver-label-check:
4868
name: Semantic Version label check
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/bin/bash
2+
##===----------------------------------------------------------------------===##
3+
##
4+
## This source file is part of the SwiftAWSLambdaRuntime open source project
5+
##
6+
## Copyright (c) 2017-2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors
7+
## Licensed under Apache License v2.0
8+
##
9+
## See LICENSE.txt for license information
10+
## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
11+
##
12+
## SPDX-License-Identifier: Apache-2.0
13+
##
14+
##===----------------------------------------------------------------------===##
15+
16+
# This script validates the packaging plugins on Swift 6.4+, where the
17+
# 'archive' verb is reimplemented to delegate to the AWSLambdaPluginHelper and
18+
# the new 'lambda-build' verb is available. It exercises BOTH verbs against the
19+
# example to prove:
20+
# 1. the legacy 'archive' surface still produces a valid bootstrap + ZIP
21+
# (existing behaviour is preserved on the new code path), and
22+
# 2. the new 'lambda-build' verb produces an equivalent artifact.
23+
24+
set -euo pipefail
25+
26+
log() { printf -- "** %s\n" "$*" >&2; }
27+
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
28+
fatal() { error "$@"; exit 1; }
29+
30+
test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset"
31+
32+
# Use the local checkout of swift-aws-lambda-runtime instead of the published release
33+
.github/workflows/scripts/use-local-deps.sh "Examples/${EXAMPLE}/Package.swift"
34+
35+
# The product name is "MyLambda" in both HelloWorld and ResourcesPackaging.
36+
PRODUCT=MyLambda
37+
38+
pushd "Examples/${EXAMPLE}" >/dev/null || exit 1
39+
40+
# ---------------------------------------------------------------------------
41+
# Assert that a given output directory contains a valid Linux bootstrap and ZIP.
42+
# $1: human-readable label of the verb under test
43+
# $2: path to the plugin output directory (containing <PRODUCT>/)
44+
# ---------------------------------------------------------------------------
45+
verify_output() {
46+
local label="$1"
47+
local output_dir="$2"
48+
local bootstrap="${output_dir}/${PRODUCT}/bootstrap"
49+
local zip_file="${output_dir}/${PRODUCT}/${PRODUCT}.zip"
50+
51+
log "Verifying output of '${label}'"
52+
53+
# did the plugin generate a Linux binary?
54+
[ -f "${bootstrap}" ] || fatal "${label}: bootstrap not found at ${bootstrap}"
55+
file "${bootstrap}" | grep --silent ELF || fatal "${label}: bootstrap is not an ELF binary"
56+
57+
# did the plugin create a ZIP file?
58+
[ -f "${zip_file}" ] || fatal "${label}: ZIP not found at ${zip_file}"
59+
60+
# does the ZIP file contain the bootstrap?
61+
unzip -l "${zip_file}" | grep --silent bootstrap || fatal "${label}: bootstrap missing from ZIP"
62+
63+
# if EXAMPLE is ResourcesPackaging, check the ZIP file contains hello.txt
64+
if [ "$EXAMPLE" == "ResourcesPackaging" ]; then
65+
log "${label}: checking that the resource was added to the ZIP file"
66+
if unzip -l "${zip_file}" | grep --silent hello.txt; then
67+
log "${label}: resource found."
68+
else
69+
fatal "${label}: resource hello.txt not found in ZIP."
70+
fi
71+
fi
72+
73+
log "${label}: output is OK for example ${EXAMPLE}"
74+
}
75+
76+
# ---------------------------------------------------------------------------
77+
# 1. Legacy 'archive' verb (now delegates to AWSLambdaPluginHelper on 6.4+)
78+
# ---------------------------------------------------------------------------
79+
log "Testing 'archive' verb"
80+
ARCHIVE_OUTPUT_DIR=.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager
81+
LAMBDA_USE_LOCAL_DEPS=../.. swift package archive \
82+
--allow-network-connections docker \
83+
--base-docker-image swift:amazonlinux2023 || fatal "'archive' verb failed"
84+
verify_output "archive" "${ARCHIVE_OUTPUT_DIR}"
85+
86+
# ---------------------------------------------------------------------------
87+
# 2. New 'lambda-build' verb (Swift 6.4+ only)
88+
# ---------------------------------------------------------------------------
89+
log "Testing 'lambda-build' verb"
90+
BUILD_OUTPUT_DIR=.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder
91+
LAMBDA_USE_LOCAL_DEPS=../.. swift package lambda-build \
92+
--allow-network-connections docker \
93+
--products "${PRODUCT}" \
94+
--base-docker-image swift:amazonlinux2023 || fatal "'lambda-build' verb failed"
95+
verify_output "lambda-build" "${BUILD_OUTPUT_DIR}"
96+
97+
echo "✅ Both 'archive' and 'lambda-build' are OK with example ${EXAMPLE} on Swift 6.4"
98+
popd >/dev/null || exit 1

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Package.resolved
1212
.vscode
1313
.devcontainer
1414
.amazonq
15-
.kiro
1615
nodejs
17-
.ash
16+
.ash
17+
.kiro/
18+
.claude/

.licenseignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ Package.resolved
3636
**/.npmignore
3737
**/*.json
3838
**/*.txt
39-
*.toml
39+
*.toml
40+
.kiro/*

.swiftformatignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Sources/AWSLambdaPluginHelper/lambda-init/Template.swift

Examples/APIGatewayV1/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ To build the package, type the following commands.
2525

2626
```bash
2727
swift build
28-
swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023
28+
swift package --allow-network-connections docker lambda-build
2929
```
3030

3131
If there is no error, there is a ZIP file ready to deploy.
32-
The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip`
32+
The ZIP file is located at `.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder/APIGatewayLambda/APIGatewayLambda.zip`
3333

3434
## Deploy
3535

Examples/APIGatewayV1/template.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Resources:
2323
APIGatewayLambda:
2424
Type: AWS::Serverless::Function
2525
Properties:
26-
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip
26+
CodeUri: .build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder/APIGatewayLambda/APIGatewayLambda.zip
2727
Timeout: 60
2828
Handler: swift.bootstrap # ignored by the Swift runtime
2929
Runtime: provided.al2023

Examples/APIGatewayV2+LambdaAuthorizer/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ To build the package, type the following commands.
1919

2020
```bash
2121
swift build
22-
swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023
22+
swift package --allow-network-connections docker lambda-build
2323
```
2424

2525
If there is no error, there are two ZIP files ready to deploy, one for the authorizer function and one for the business function.
26-
The ZIP file are located under `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager`
26+
The ZIP file are located under `.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder`
2727

2828
## Deploy
2929

Examples/APIGatewayV2+LambdaAuthorizer/template.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Resources:
4747
APIGatewayLambda:
4848
Type: AWS::Serverless::Function
4949
Properties:
50-
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip
50+
CodeUri: .build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder/APIGatewayLambda/APIGatewayLambda.zip
5151
Timeout: 60
5252
Handler: swift.bootstrap # ignored by the Swift runtime
5353
Runtime: provided.al2023
@@ -72,7 +72,7 @@ Resources:
7272
AuthorizerLambda:
7373
Type: AWS::Serverless::Function
7474
Properties:
75-
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/AuthorizerLambda/AuthorizerLambda.zip
75+
CodeUri: .build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder/AuthorizerLambda/AuthorizerLambda.zip
7676
Timeout: 29 # max 29 seconds for Lambda authorizers
7777
Handler: swift.bootstrap # ignored by the Swift runtime
7878
Runtime: provided.al2023

0 commit comments

Comments
 (0)