From e66e105f3e73438c97e32f8783684fb29b1c32c1 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 29 Jun 2026 14:34:31 +0200 Subject: [PATCH 1/4] [plugin] fluh print() message in the plugin helper --- .../AWSLambdaPluginHelper.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift index 70a86435..03868b78 100644 --- a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift @@ -13,6 +13,18 @@ // //===----------------------------------------------------------------------===// +#if os(macOS) +import Darwin.C +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import ucrt +#else +#error("Unsupported platform") +#endif + @main @available(LambdaSwift 2.0, *) struct AWSLambdaPluginHelper { @@ -24,6 +36,11 @@ struct AWSLambdaPluginHelper { } public static func main() async throws { + // SwiftPM runs plugins with stdout connected to a pipe rather than a TTY, so the C runtime + // block-buffers stdout and the helper's output (and --help text) only appears when the process + // exits. Force line buffering so each printed line streams to the user as it is produced. + setvbuf(stdout, nil, _IOLBF, 0) + let args = CommandLine.arguments let helper = AWSLambdaPluginHelper() From c7aa33d5f53ef7030dbddda99ce50d798630fa91 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 29 Jun 2026 15:05:39 +0200 Subject: [PATCH 2/4] [plugin] Fix Linux build: @preconcurrency import for libc stdout global On Glibc/Musl, stdout is an `extern FILE *` global var, which Swift 6 strict concurrency rejects as "not concurrency-safe" when passed to setvbuf. Import the platform libc with @preconcurrency to suppress the diagnostic for that symbol. Verified with `swift build --target AWSLambdaPluginHelper` in the swiftlang/swift:nightly-6.4.x-bookworm container. Co-Authored-By: Claude Opus 4.8 --- .../AWSLambdaPluginHelper.swift | 10 +- oci-backend-plan.md | 360 ++++++++++++++++++ 2 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 oci-backend-plan.md diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift index 03868b78..6cd64346 100644 --- a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift @@ -13,14 +13,16 @@ // //===----------------------------------------------------------------------===// +// `@preconcurrency` suppresses the Swift 6 strict-concurrency diagnostic for the libc `stdout` +// global, which is an `extern FILE *` (a mutable global var) on Glibc/Musl. #if os(macOS) -import Darwin.C +@preconcurrency import Darwin.C #elseif canImport(Glibc) -import Glibc +@preconcurrency import Glibc #elseif canImport(Musl) -import Musl +@preconcurrency import Musl #elseif os(Windows) -import ucrt +@preconcurrency import ucrt #else #error("Unsupported platform") #endif diff --git a/oci-backend-plan.md b/oci-backend-plan.md new file mode 100644 index 00000000..a6a94847 --- /dev/null +++ b/oci-backend-plan.md @@ -0,0 +1,360 @@ +# Design plan: OCI image archive backend + ECR deploy + +Status: design / requirements. No code yet. Builds on the merged `ArchiveBackend` +work (#681): `--archive-format zip|oci` already parses, `oci` is recognised but +throws `unsupportedArchiveFormat`. This plan turns `oci` on end-to-end. + +## Goal + +Let `lambda-build --archive-format oci` produce a Lambda-compatible **OCI image**, +and let `lambda-deploy` push it to **Amazon ECR** and create/update an +`PackageType=Image` function — without introducing a hard Docker dependency +(Apple's `container` must work too). + +--- + +## Verified facts (checked against the codebase + AWS docs, 2026-06-29) + +1. **Lambda accepts OCI images, not just Docker.** Lambda supports "Docker image + manifest V2 schema 2 **or** OCI Specifications v1.0.0+". So `container`-built OCI + images are valid. No hard Docker dependency on the Lambda side. + (Source: docs/lambda images-create — "Using a non-AWS base image".) +2. **Deploy target must be ECR, same Region; local is test-only.** "Build your image + locally and upload it to an Amazon ECR repository… must be in the same AWS Region + as the function." Local images are only usable for testing via the Runtime + Interface Emulator (RIE), never for deployment. +3. **Package type is immutable.** "You cannot change the deployment package type + (.zip or container image) for an existing function… You must create a new + function." → the deployer must detect a package-type mismatch on update and + error with guidance (delete + redeploy), reusing the pattern from the + execution-role check (#676). +4. **The image push is NOT an AWS API call.** ECR auth + repo management are API + calls (ECR client); `docker push` / `container image push` use the OCI registry + protocol and must shell out to the resolved container CLI. +5. **Base image:** `public.ecr.aws/lambda/provided:al2023` (OS-only; bundles the RIE). + Our compiled binary is the `bootstrap` entrypoint; Swift already speaks the + Runtime API, so no extra runtime-interface-client is needed. +6. **The generated AWS clients are ZIP-only, and are generated — never hand-edited.** + Confirmed in `GeneratedClients/Lambda/LambdaShapes.swift` (header: "Generated by + scripts/generate-aws-clients.sh — DO NOT EDIT"; git shows no post-generation hand-edits): + - `LambdaPackageType` enum exists (`.zip` / `.image`) ✅ + - `FunctionCode` has `zipFile` / `s3Bucket` / `s3Key` but **NO `imageUri`** ❌ + - `UpdateFunctionCodeRequest` has zip/S3 fields but **NO `imageUri`** ❌ + - `CreateFunctionRequest.runtime` is **non-optional** `LambdaRuntime` and + `handler` is non-optional — but **image functions must omit runtime & handler** ❌ + - The deployer hardcodes `runtime: .providedAl2023, handler: ..., packageType: .zip`. + → The Lambda client AND the deployer need changes, not just "add ECR". + → **HARD RULE: generated files are changed ONLY by editing + `scripts/generate-aws-clients.sh` and re-running it. Never hand-edit + `GeneratedClients/*`.** The image fields must come out of the generator (the + Smithy model has them); if soto-codegenerator omits/trims them, fix it via the + script's config or a post-processing step — not by patching the `.swift`. +7. **There is no ECR client today** (`GeneratedClients/` = IAM, Lambda, S3, STS). +8. **ECR Smithy model exists upstream** in `aws-sdk-go-v2` + (`aws-models/ecr.json`, valid Smithy 2.0) with `GetAuthorizationToken`, + `CreateRepository`, `DescribeRepositories`, `SetRepositoryPolicy` — so the + existing `generate-aws-clients.sh` can produce it. + +--- + +## Requirements + +### Functional +- R1. `lambda-build --archive-format oci` builds an OCI image for the Lambda target + (Amazon Linux 2023, single arch matching `--architecture`/host) from the compiled + product, using the **same container CLI** already selected for cross-compilation + (docker or container), via `--cross-compile-tool-path`. +- R2. The image uses a **minimal Amazon Linux 2023 base** (e.g. + `public.ecr.aws/amazonlinux/amazonlinux:2023-minimal`), NOT `provided.al2023`. + Rationale (verified against the images-create doc): + * The Swift runtime in our `bootstrap` already speaks the Lambda Runtime API, so we + ARE the required "runtime interface client" — we don't need one from a base image. + * `provided.al2023`'s only Lambda-specific content is the Runtime Interface Emulator + (RIE) — documented as "for local testing" — plus entrypoint plumbing we replace. + There is no production-time Lambda optimization baked in that we'd lose. + * Minimal AL2023 is smaller / fewer layers (the doc notes small manifests & few + layers help cold-start optimization) and gives **glibc parity** with the build + image (`swift:*-amazonlinux2023`), the same reason AL2023 is the ZIP default. + The Dockerfile copies the built executable in as the entrypoint and sets + `ENTRYPOINT ["/var/runtime/bootstrap"]` (exact path TBD/verified), readable by the + default Lambda user (no `USER`), runnable on a read-only FS with writable `/tmp`. + Trade-offs we accept: we own the ENTRYPOINT, and there's no bundled RIE (add it + separately later if/when we want `container run` local testing). + OPEN: confirm the exact bootstrap path + ENTRYPOINT form against a real + create-function + invoke on a minimal base (the earlier E2E test used + `provided:al2023`; re-verify with minimal AL2023 before finalising). +- R3. `lambda-deploy` for an OCI artifact: ensure ECR repo exists → obtain auth token → + `login` → `tag` → `push` → create/update function with `PackageType=Image` and + `Code.ImageUri`. +- R4. On update, if the existing function's package type ≠ the artifact's, stop with a + clear error + suggested action (delete & redeploy). (Package type is immutable.) +- R5. Ensure the ECR repository policy lets Lambda pull (`BatchGetImage`, + `GetDownloadUrlForLayer`); rely on Lambda's auto-add when the caller has + `Get/SetRepositoryPolicy`, else set it explicitly. + +### Non-functional / constraints +- R6. No hard Docker dependency: every container step (build/tag/login/push) must work + through Apple `container` as well as docker. CLI-specific argv lives in `ContainerCLI` + (no shared helper — CLIs may diverge; same rule as the build argv). +- R7. FoundationEssentials-safe (no `NS*` APIs); Linux-buildable. +- R8. Maintainer-run codegen stays generate-and-commit (not in the build path). +- R9. Default behaviour unchanged: `--archive-format` defaults to `zip`; ZIP path + byte-for-byte identical. + +--- + +## Work breakdown (suggested as TWO stacked PRs) + +### PR-A — OCI build/archive (no deploy) +Self-contained in `lambda-build`; ends with an OCI image in the local store / a +pushable reference. Does NOT deploy. + +1. **`ContainerCLI` gains image-packaging argv** (one method per concern, implemented + independently in `DockerCLI` and `AppleContainerCLI`, no shared helper): + - `buildImageArguments(dockerfile:contextDir:tag:architecture:)` + - (tag/login/push deferred to PR-B, or stub here if convenient) +2. **`OCIArchiveBackend: ArchiveBackend`** under `ArchiveBackends/`: + - generates a minimal Dockerfile (FROM provided:al2023, COPY bootstrap, CMD/handler), + - runs ` build` via the resolved `crossCompileToolPath`, + - tags the image `lambda/:latest` (local). +3. **Artifact model decision (needed here):** today `ArchiveBackend.archive` returns + `[String: URL]` (a file path). An OCI image is an *image reference*, not a file. + → Generalise to an `enum Artifact { case zip(URL); case ociImage(reference: String) }` + and return `[String: Artifact]`. Update `ZipArchiveBackend` to return `.zip(url)`. + (Chosen over a throwaway `docker save` tarball: the deploy path wants the tag, not a file.) +4. **Flip `oci` to supported** in `ArchiveFormat.isSupported` + `makeArchiveBackend()`. +5. Tests: Dockerfile-generation golden test; `buildImageArguments` golden tests per CLI; + `makeArchiveBackend(.oci)` returns `OCIArchiveBackend`. +6. Semver: **minor** (new public CLI behaviour for `--archive-format oci`). + +### PR-B — ECR push + Image deploy +The larger half; touches codegen, the Lambda client, and the deployer. + +7. **Codegen: add ECR** to `scripts/generate-aws-clients.sh`: + - `SERVICE_OPERATIONS["ECR"]="GetAuthorizationToken,CreateRepository,DescribeRepositories,SetRepositoryPolicy,GetRepositoryPolicy"` + - `SERVICE_MODEL_DIRS["ECR"]="ecr"` + - add matching block to the hardcoded JSON in `generate_config()` (or, better, generate + that JSON from the bash array to kill the existing duplication). + - run script → commit `GeneratedClients/ECR/`. +8. **Extend the Lambda client for images — via the script only** (never hand-edit + `GeneratedClients/`). Re-run `generate-aws-clients.sh` and confirm the regenerated + shapes include: + - `FunctionCode.imageUri` + - `UpdateFunctionCodeRequest.imageUri` + - `CreateFunctionRequest.runtime` and `handler` as optional (image fns omit them). + - `LambdaPackageType` (already present). + If the codegen output still lacks any of these (the current shapes are missing them + today), the fix goes in the script — its operation list, the `generate_config()` + JSON, or a post-processing step (like `add_availability_annotations`) — not in the + generated `.swift`. Investigate first whether the current trimming is from the + `--operations` filter, the JSON config, or a soto-codegenerator limitation; that + determines whether it's a config tweak or a codegen-side fix. +9. **Deploy strategy split** (mirror the build/archive backend pattern): a Zip path + (current behaviour) and an Image path: + - ensure ECR repo (DescribeRepositories → CreateRepository; mirrors S3 + HeadBucket/CreateBucket), + - `GetAuthorizationToken` → ` login` → ` tag` → ` push` (shell, via + ContainerCLI argv), + - `CreateFunction`/`UpdateFunctionCode` with `packageType: .image`, `code.imageUri`, + no runtime/handler, + - set/verify ECR repo policy for Lambda pull (R5). +10. **Package-type-immutability guard** (R4) on the update path. +11. **`ContainerCLI`**: add `tagArguments`, `loginArguments(registry:username:password:)`, + `pushArguments` — per-CLI, independent. **Risk flag:** Apple `container`'s ECR + login/push subcommands are newer/less proven than docker's; verify against a real + ECR push before finalising (do not assume parity). +12. Deploy flags as needed: `--ecr-repository ` (default derived from function + name, like the S3 bucket naming), maybe `--image-tag`. +13. Tests + docs (`using-the-spm-plugins.md`: document `--archive-format oci`, ECR + prerequisites, IAM permissions from the doc). +14. Semver: **minor**. + +--- + +## Cross-cutting designs (previously missing) + +### D1. Which container CLI — docker vs Apple `container` — at *deploy* time + +Today only `lambda-build` knows the CLI: the plugin resolves docker-or-container from +`--cross-compile`/`--container-cli` and passes `--cross-compile-tool-path` to the +helper (see BuildBackends). `lambda-deploy` has **no** container-CLI concept — it only +uploads a zip. For OCI, *deploy* also needs a container CLI (to `login`/`tag`/`push`, +and — per the verified findings — to reason about the index-vs-flat manifest). + +Decision: +- Add the **same flag pair to `lambda-deploy`**: `--cross-compile ` + (alias `--container-cli`), and have the `AWSLambdaDeployer` plugin resolve the tool + via `context.tool(named:)` exactly like `AWSLambdaBuilder`, forwarding + `--cross-compile-tool-path` to the helper. Reuse the **same `ContainerCLI` protocol + + `DockerCLI`/`AppleContainerCLI`** types (they already encapsulate per-CLI argv; we add + `login`/`tag`/`push` methods there). +- Default when omitted: docker (matches build's default), but if the build step + recorded which CLI it used (see D2 manifest), deploy should **default to the CLI named + in the manifest** so a `container`-built image is pushed with `container`. Explicit + `--cross-compile` on deploy overrides. +- The container-specific index-unwrap (resolve `@digest` of the child manifest before + `create/update-function`) lives behind the CLI abstraction: only the + `AppleContainerCLI` path needs it; docker produces a flat manifest. Model it as a + capability on `ContainerCLI`, e.g. `producesImageIndex: Bool` (or a + `resolveDeployableReference(...)` hook), so the deploy logic stays CLI-agnostic. + +### D2. Handing data from `lambda-build` to `lambda-deploy` + +Today the contract is an **implicit filesystem convention**: build writes +`.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder//.zip`, and +deploy reads exactly that path (Deployer.swift ~688-709), overridable with +`--input-directory`. There is no metadata channel — deploy re-derives everything +(function name = product, package type hardcoded `.zip`). + +For OCI the artifact is **not a file** — it's an image reference (`@digest`), plus +we must know: package type (zip vs image), the ECR repo/URI, the resolved child-manifest +digest, the architecture, and which CLI built it. None of that is recoverable from a +filesystem path. So we need an explicit **build manifest**. + +Decision — write a small JSON descriptor next to the existing output dir: +`.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder//build-manifest.json`, +e.g.: +```json +{ + "schemaVersion": 1, + "product": "MyLambda", + "packageType": "image", // "zip" | "image" + "architecture": "arm64", // x64 | arm64 + "containerCLI": "container", // docker | container | null (for zip) + "zipPath": null, // set for packageType=zip + "imageReference": "486652...dkr.ecr.eu-central-1.amazonaws.com/myrepo@sha256:", + "imageTag": "myrepo:latest" // informational +} +``` +- `ZipArchiveBackend` writes the manifest with `packageType: zip`, `zipPath`. **Backwards + compatible**: if no manifest is found, deploy falls back to today's path convention + + `.zip` assumption, so existing flows and the legacy `archive` plugin still work. +- `OCIArchiveBackend` writes `packageType: image` + `imageReference` (the already + index-unwrapped `@digest` — see findings) + `containerCLI` + `architecture`. +- `lambda-deploy` reads the manifest first; `--input-directory` still overrides the + lookup root. Deploy branches on `packageType`: zip → current flow; image → ECR/Image + flow, using `imageReference` directly (no re-resolution needed if build already + unwrapped the digest). `--cross-compile` on deploy defaults from `containerCLI`. +- Keeps the plugins thin: the manifest is produced/consumed by the **helper**, not the + SwiftPM plugins. This also finally makes the build→deploy contract explicit instead of + a hardcoded path string duplicated on both sides. + +RESOLVED — `login`/ECR-`tag`/`push` happen at **deploy** time, not build. Decided by the +plugins' sandbox permissions + credential split (verified in `Package@swift-6.4.swift`): +- `lambda-build` network scope = `.docker` (Docker socket only); it never touches AWS + credentials/STS. It is intentionally **offline-except-Docker**. +- `lambda-deploy` network scope = `.all(ports: [443])`; it already resolves AWS account + via STS and calls AWS APIs. + +ECR `login`+`push` need HTTPS-to-ECR (443) **and** AWS creds **and** region/account — all +deploy-only. Pushing from build would require widening build's sandbox to 443 + giving it +AWS creds, breaking the clean split. The correct analogy to ZIP: build *produces a local +artifact*, deploy *uploads it* (zip → S3/direct upload is already a deploy step; image → +ECR push is the same role). + +Step ownership: +- **build**: `build` the image + a **local** `tag` (`swift-lambda/:latest`, no + creds). Writes manifest with `packageType: image`, local `imageTag`, `containerCLI`, + `architecture`. No `imageReference` yet, no push. +- **deploy**: ensure ECR repo → `get-login-password`→`registry login` → ECR-qualified + re-`tag` (`.dkr.ecr.…`, needs account+region) → `push` → unwrap index to + child `@digest` → `create/update-function`. + +Consequence to document: `lambda-build --archive-format oci` alone does NOT push — the +image reaches ECR only on `lambda-deploy` (symmetric with zip: build makes it, deploy +uploads it). A build-time `--push` is intentionally NOT added (would force widening +build's sandbox + creds); revisit only on concrete CI demand. + +### D3. Cross-architecture build/deploy (lower priority) + +Today: `lambda-build` has **no `--architecture` flag** — the cross-compile container runs +under the host/daemon's default arch, and the produced binary's arch is whatever the +base image resolves to. `lambda-deploy` *does* have `--architecture` (default +`Architecture.host` via `#if arch`), passed to `CreateFunction.architectures`. So a +mismatch is already possible today (build arm64, deploy declares x64) and simply produces +a broken function — it's just latent. + +For correctness (and required for OCI, where image arch is baked in): +- Add `--architecture ` to **`lambda-build`** too, default host. Thread it into + the container build: docker `--platform linux/`, container `--arch ` + (verified `container` accepts `--arch`/`--platform`). The base `swift:*-amazonlinux2023` + is multi-arch so cross-building works via the daemon's emulation (qemu / Rosetta-backed + VM); note this is slow and may need the user's runtime to support emulation. +- Record `architecture` in the build manifest (D2); `lambda-deploy` **defaults its + `--architecture` from the manifest** instead of host, removing the latent mismatch. + Explicit `--architecture` on either side overrides; deploy should **error if its + declared arch ≠ the manifest arch** (can't deploy an arm64 image as x64). +- OCI specific: the function `--architectures` MUST match the image's single arch; since + `container` bakes one arch into the (unwrapped) child manifest, deploy derives it from + the manifest rather than guessing. +- Emulation availability (building x64 on an arm64 Mac and vice-versa) is environment + dependent; out of scope to guarantee — document it and surface the underlying CLI error + if emulation isn't set up. + +## Open questions for sign-off +- Q1. Artifact model: confirm the `enum Artifact` approach (PR-A item 3) vs keeping + `[String: URL]` and writing a `docker save` tarball. Plan assumes the enum. +- Q2. RESOLVED by dry-run (2026-06-29): the full soto-codegenerator output ALREADY + contains every OCI field — `FunctionCode.imageUri`, `UpdateFunctionCodeRequest.imageUri`, + and optional `runtime`/`handler` on `CreateFunctionRequest`. So image support is a pure + REGENERATION via the script, no hand-edits and no post-processing hack. BUT see Q5. +- Q5. NEW (blocker found in dry-run): `scripts/generate-aws-clients.sh` is out of date + with the current soto-codegenerator and will NOT reproduce the committed output as-is: + * CLI flags changed: now `--input-file` / `--output-folder` / `--config ` + (the script's `--model-path` / `--operations` / `--module` no longer apply; + `--operations` is ignored). + * Operation names in the config must be **camelCase** (`createFunction`), keyed by + the **model filename** (`lambda`). PascalCase matches nothing and emits only the + error type — likely how the committed clients ended up trimmed/ZIP-only. + → The script must be fixed (flags + camelCase config + filename keys) BEFORE or AS PART + OF the ECR/Lambda regen. This is itself a small standalone PR worth doing first: + "fix generate-aws-clients.sh for current soto-codegenerator" — re-running it should + then regenerate IAM/Lambda/S3/STS cleanly (and pick up the image fields for Lambda). +- Q3. One combined PR or the A/B split above? Recommend split — PR-A is shippable and + testable without any AWS account; PR-B needs ECR + real push verification. +- Q4. Scope of ECR ops: is `DescribeRepositories`+`CreateRepository`+`Get/SetRepositoryPolicy` + +`GetAuthorizationToken` the right minimal set, or also `CreateRepository` idempotency + via `RepositoryAlreadyExistsException` handling (like the S3 bucket path)? + +## Verified end-to-end with Apple `container` (2026-06-29, account 486652066693, eu-central-1) + +Built a minimal `FROM public.ecr.aws/lambda/provided:al2023` image with Apple +`container` (docker daemon was DOWN), pushed to ECR, created a Lambda function. +Findings: + +- **`container build` needs an explicit context dir** — the default `.` misbehaved + ("transferring context: 2B", COPY couldn't find files). Always pass `` + and `-f ` explicitly. +- **ECR login works identically to docker**: `aws ecr get-login-password | container + registry login --username AWS --password-stdin ` → "Login succeeded". +- **`container image tag` / `push` work** against ECR (39.6 MB pushed fine). +- **🚫 BLOCKER: `container` always exports an OCI image *index*** manifest + (`application/vnd.oci.image.index.v1+json`), even with `--arch arm64` / + `--platform linux/arm64` on build AND push. The index wraps a single linux/arm64 + child manifest (`application/vnd.oci.image.manifest.v1+json`). +- **Lambda REJECTS the index**: `create-function` with the index tag fails with + `InvalidParameterValueException: The image manifest, config or layer media type ... + is not supported`. (Lambda does not accept multi-arch / index manifests.) +- **✅ WORKAROUND (proven):** point Lambda at the **child manifest by digest**, not the + index tag: `ImageUri = @sha256:`. `create-function` then + succeeds (State: Pending → Active). Get the child digest from the pushed index via + `ecr batch-get-image ... imageTag=latest` → `imageManifest.manifests[0].digest`. + +**Design consequence:** the OCI deploy path must, after push, resolve the index to its +single child manifest digest and deploy *that* `@digest` as the `ImageUri` — not the +tag. (Alternatively, find a way to make `container` push a flat manifest; none of the +build/push `--arch`/`--platform` flags did so in testing. Digest-of-child is the +reliable path today.) For docker, a normal `docker build`/`push` produces a flat +`vnd.oci.image.manifest`/`docker v2s2` manifest that Lambda accepts directly by tag — +so this index-unwrap step is **container-specific** and belongs in `AppleContainerCLI` +or the deploy logic, gated on which CLI produced the image. + +## Risks +- ~~Apple `container` ECR login/push parity~~ — VERIFIED working (see above). The real + issue turned out to be the OCI-index manifest, with a proven digest-based workaround. +- Lambda client regeneration could surface other trimmed-shape gaps; keep the diff + reviewable. +- `provided:al2023` base image + single-arch requirement: the built binary's arch must + match the function arch and the base image arch — thread `--architecture` through. +- Image optimization latency: Image functions go Pending→Active after push; deploy + success reporting should account for it (poll state, like a future enhancement). From b803972473b3db57643ed3f655e12c2ce7730e3e Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 29 Jun 2026 15:05:50 +0200 Subject: [PATCH 3/4] Remove oci-backend-plan.md accidentally added to this branch Co-Authored-By: Claude Opus 4.8 --- oci-backend-plan.md | 360 -------------------------------------------- 1 file changed, 360 deletions(-) delete mode 100644 oci-backend-plan.md diff --git a/oci-backend-plan.md b/oci-backend-plan.md deleted file mode 100644 index a6a94847..00000000 --- a/oci-backend-plan.md +++ /dev/null @@ -1,360 +0,0 @@ -# Design plan: OCI image archive backend + ECR deploy - -Status: design / requirements. No code yet. Builds on the merged `ArchiveBackend` -work (#681): `--archive-format zip|oci` already parses, `oci` is recognised but -throws `unsupportedArchiveFormat`. This plan turns `oci` on end-to-end. - -## Goal - -Let `lambda-build --archive-format oci` produce a Lambda-compatible **OCI image**, -and let `lambda-deploy` push it to **Amazon ECR** and create/update an -`PackageType=Image` function — without introducing a hard Docker dependency -(Apple's `container` must work too). - ---- - -## Verified facts (checked against the codebase + AWS docs, 2026-06-29) - -1. **Lambda accepts OCI images, not just Docker.** Lambda supports "Docker image - manifest V2 schema 2 **or** OCI Specifications v1.0.0+". So `container`-built OCI - images are valid. No hard Docker dependency on the Lambda side. - (Source: docs/lambda images-create — "Using a non-AWS base image".) -2. **Deploy target must be ECR, same Region; local is test-only.** "Build your image - locally and upload it to an Amazon ECR repository… must be in the same AWS Region - as the function." Local images are only usable for testing via the Runtime - Interface Emulator (RIE), never for deployment. -3. **Package type is immutable.** "You cannot change the deployment package type - (.zip or container image) for an existing function… You must create a new - function." → the deployer must detect a package-type mismatch on update and - error with guidance (delete + redeploy), reusing the pattern from the - execution-role check (#676). -4. **The image push is NOT an AWS API call.** ECR auth + repo management are API - calls (ECR client); `docker push` / `container image push` use the OCI registry - protocol and must shell out to the resolved container CLI. -5. **Base image:** `public.ecr.aws/lambda/provided:al2023` (OS-only; bundles the RIE). - Our compiled binary is the `bootstrap` entrypoint; Swift already speaks the - Runtime API, so no extra runtime-interface-client is needed. -6. **The generated AWS clients are ZIP-only, and are generated — never hand-edited.** - Confirmed in `GeneratedClients/Lambda/LambdaShapes.swift` (header: "Generated by - scripts/generate-aws-clients.sh — DO NOT EDIT"; git shows no post-generation hand-edits): - - `LambdaPackageType` enum exists (`.zip` / `.image`) ✅ - - `FunctionCode` has `zipFile` / `s3Bucket` / `s3Key` but **NO `imageUri`** ❌ - - `UpdateFunctionCodeRequest` has zip/S3 fields but **NO `imageUri`** ❌ - - `CreateFunctionRequest.runtime` is **non-optional** `LambdaRuntime` and - `handler` is non-optional — but **image functions must omit runtime & handler** ❌ - - The deployer hardcodes `runtime: .providedAl2023, handler: ..., packageType: .zip`. - → The Lambda client AND the deployer need changes, not just "add ECR". - → **HARD RULE: generated files are changed ONLY by editing - `scripts/generate-aws-clients.sh` and re-running it. Never hand-edit - `GeneratedClients/*`.** The image fields must come out of the generator (the - Smithy model has them); if soto-codegenerator omits/trims them, fix it via the - script's config or a post-processing step — not by patching the `.swift`. -7. **There is no ECR client today** (`GeneratedClients/` = IAM, Lambda, S3, STS). -8. **ECR Smithy model exists upstream** in `aws-sdk-go-v2` - (`aws-models/ecr.json`, valid Smithy 2.0) with `GetAuthorizationToken`, - `CreateRepository`, `DescribeRepositories`, `SetRepositoryPolicy` — so the - existing `generate-aws-clients.sh` can produce it. - ---- - -## Requirements - -### Functional -- R1. `lambda-build --archive-format oci` builds an OCI image for the Lambda target - (Amazon Linux 2023, single arch matching `--architecture`/host) from the compiled - product, using the **same container CLI** already selected for cross-compilation - (docker or container), via `--cross-compile-tool-path`. -- R2. The image uses a **minimal Amazon Linux 2023 base** (e.g. - `public.ecr.aws/amazonlinux/amazonlinux:2023-minimal`), NOT `provided.al2023`. - Rationale (verified against the images-create doc): - * The Swift runtime in our `bootstrap` already speaks the Lambda Runtime API, so we - ARE the required "runtime interface client" — we don't need one from a base image. - * `provided.al2023`'s only Lambda-specific content is the Runtime Interface Emulator - (RIE) — documented as "for local testing" — plus entrypoint plumbing we replace. - There is no production-time Lambda optimization baked in that we'd lose. - * Minimal AL2023 is smaller / fewer layers (the doc notes small manifests & few - layers help cold-start optimization) and gives **glibc parity** with the build - image (`swift:*-amazonlinux2023`), the same reason AL2023 is the ZIP default. - The Dockerfile copies the built executable in as the entrypoint and sets - `ENTRYPOINT ["/var/runtime/bootstrap"]` (exact path TBD/verified), readable by the - default Lambda user (no `USER`), runnable on a read-only FS with writable `/tmp`. - Trade-offs we accept: we own the ENTRYPOINT, and there's no bundled RIE (add it - separately later if/when we want `container run` local testing). - OPEN: confirm the exact bootstrap path + ENTRYPOINT form against a real - create-function + invoke on a minimal base (the earlier E2E test used - `provided:al2023`; re-verify with minimal AL2023 before finalising). -- R3. `lambda-deploy` for an OCI artifact: ensure ECR repo exists → obtain auth token → - `login` → `tag` → `push` → create/update function with `PackageType=Image` and - `Code.ImageUri`. -- R4. On update, if the existing function's package type ≠ the artifact's, stop with a - clear error + suggested action (delete & redeploy). (Package type is immutable.) -- R5. Ensure the ECR repository policy lets Lambda pull (`BatchGetImage`, - `GetDownloadUrlForLayer`); rely on Lambda's auto-add when the caller has - `Get/SetRepositoryPolicy`, else set it explicitly. - -### Non-functional / constraints -- R6. No hard Docker dependency: every container step (build/tag/login/push) must work - through Apple `container` as well as docker. CLI-specific argv lives in `ContainerCLI` - (no shared helper — CLIs may diverge; same rule as the build argv). -- R7. FoundationEssentials-safe (no `NS*` APIs); Linux-buildable. -- R8. Maintainer-run codegen stays generate-and-commit (not in the build path). -- R9. Default behaviour unchanged: `--archive-format` defaults to `zip`; ZIP path - byte-for-byte identical. - ---- - -## Work breakdown (suggested as TWO stacked PRs) - -### PR-A — OCI build/archive (no deploy) -Self-contained in `lambda-build`; ends with an OCI image in the local store / a -pushable reference. Does NOT deploy. - -1. **`ContainerCLI` gains image-packaging argv** (one method per concern, implemented - independently in `DockerCLI` and `AppleContainerCLI`, no shared helper): - - `buildImageArguments(dockerfile:contextDir:tag:architecture:)` - - (tag/login/push deferred to PR-B, or stub here if convenient) -2. **`OCIArchiveBackend: ArchiveBackend`** under `ArchiveBackends/`: - - generates a minimal Dockerfile (FROM provided:al2023, COPY bootstrap, CMD/handler), - - runs ` build` via the resolved `crossCompileToolPath`, - - tags the image `lambda/:latest` (local). -3. **Artifact model decision (needed here):** today `ArchiveBackend.archive` returns - `[String: URL]` (a file path). An OCI image is an *image reference*, not a file. - → Generalise to an `enum Artifact { case zip(URL); case ociImage(reference: String) }` - and return `[String: Artifact]`. Update `ZipArchiveBackend` to return `.zip(url)`. - (Chosen over a throwaway `docker save` tarball: the deploy path wants the tag, not a file.) -4. **Flip `oci` to supported** in `ArchiveFormat.isSupported` + `makeArchiveBackend()`. -5. Tests: Dockerfile-generation golden test; `buildImageArguments` golden tests per CLI; - `makeArchiveBackend(.oci)` returns `OCIArchiveBackend`. -6. Semver: **minor** (new public CLI behaviour for `--archive-format oci`). - -### PR-B — ECR push + Image deploy -The larger half; touches codegen, the Lambda client, and the deployer. - -7. **Codegen: add ECR** to `scripts/generate-aws-clients.sh`: - - `SERVICE_OPERATIONS["ECR"]="GetAuthorizationToken,CreateRepository,DescribeRepositories,SetRepositoryPolicy,GetRepositoryPolicy"` - - `SERVICE_MODEL_DIRS["ECR"]="ecr"` - - add matching block to the hardcoded JSON in `generate_config()` (or, better, generate - that JSON from the bash array to kill the existing duplication). - - run script → commit `GeneratedClients/ECR/`. -8. **Extend the Lambda client for images — via the script only** (never hand-edit - `GeneratedClients/`). Re-run `generate-aws-clients.sh` and confirm the regenerated - shapes include: - - `FunctionCode.imageUri` - - `UpdateFunctionCodeRequest.imageUri` - - `CreateFunctionRequest.runtime` and `handler` as optional (image fns omit them). - - `LambdaPackageType` (already present). - If the codegen output still lacks any of these (the current shapes are missing them - today), the fix goes in the script — its operation list, the `generate_config()` - JSON, or a post-processing step (like `add_availability_annotations`) — not in the - generated `.swift`. Investigate first whether the current trimming is from the - `--operations` filter, the JSON config, or a soto-codegenerator limitation; that - determines whether it's a config tweak or a codegen-side fix. -9. **Deploy strategy split** (mirror the build/archive backend pattern): a Zip path - (current behaviour) and an Image path: - - ensure ECR repo (DescribeRepositories → CreateRepository; mirrors S3 - HeadBucket/CreateBucket), - - `GetAuthorizationToken` → ` login` → ` tag` → ` push` (shell, via - ContainerCLI argv), - - `CreateFunction`/`UpdateFunctionCode` with `packageType: .image`, `code.imageUri`, - no runtime/handler, - - set/verify ECR repo policy for Lambda pull (R5). -10. **Package-type-immutability guard** (R4) on the update path. -11. **`ContainerCLI`**: add `tagArguments`, `loginArguments(registry:username:password:)`, - `pushArguments` — per-CLI, independent. **Risk flag:** Apple `container`'s ECR - login/push subcommands are newer/less proven than docker's; verify against a real - ECR push before finalising (do not assume parity). -12. Deploy flags as needed: `--ecr-repository ` (default derived from function - name, like the S3 bucket naming), maybe `--image-tag`. -13. Tests + docs (`using-the-spm-plugins.md`: document `--archive-format oci`, ECR - prerequisites, IAM permissions from the doc). -14. Semver: **minor**. - ---- - -## Cross-cutting designs (previously missing) - -### D1. Which container CLI — docker vs Apple `container` — at *deploy* time - -Today only `lambda-build` knows the CLI: the plugin resolves docker-or-container from -`--cross-compile`/`--container-cli` and passes `--cross-compile-tool-path` to the -helper (see BuildBackends). `lambda-deploy` has **no** container-CLI concept — it only -uploads a zip. For OCI, *deploy* also needs a container CLI (to `login`/`tag`/`push`, -and — per the verified findings — to reason about the index-vs-flat manifest). - -Decision: -- Add the **same flag pair to `lambda-deploy`**: `--cross-compile ` - (alias `--container-cli`), and have the `AWSLambdaDeployer` plugin resolve the tool - via `context.tool(named:)` exactly like `AWSLambdaBuilder`, forwarding - `--cross-compile-tool-path` to the helper. Reuse the **same `ContainerCLI` protocol + - `DockerCLI`/`AppleContainerCLI`** types (they already encapsulate per-CLI argv; we add - `login`/`tag`/`push` methods there). -- Default when omitted: docker (matches build's default), but if the build step - recorded which CLI it used (see D2 manifest), deploy should **default to the CLI named - in the manifest** so a `container`-built image is pushed with `container`. Explicit - `--cross-compile` on deploy overrides. -- The container-specific index-unwrap (resolve `@digest` of the child manifest before - `create/update-function`) lives behind the CLI abstraction: only the - `AppleContainerCLI` path needs it; docker produces a flat manifest. Model it as a - capability on `ContainerCLI`, e.g. `producesImageIndex: Bool` (or a - `resolveDeployableReference(...)` hook), so the deploy logic stays CLI-agnostic. - -### D2. Handing data from `lambda-build` to `lambda-deploy` - -Today the contract is an **implicit filesystem convention**: build writes -`.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder//.zip`, and -deploy reads exactly that path (Deployer.swift ~688-709), overridable with -`--input-directory`. There is no metadata channel — deploy re-derives everything -(function name = product, package type hardcoded `.zip`). - -For OCI the artifact is **not a file** — it's an image reference (`@digest`), plus -we must know: package type (zip vs image), the ECR repo/URI, the resolved child-manifest -digest, the architecture, and which CLI built it. None of that is recoverable from a -filesystem path. So we need an explicit **build manifest**. - -Decision — write a small JSON descriptor next to the existing output dir: -`.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaBuilder//build-manifest.json`, -e.g.: -```json -{ - "schemaVersion": 1, - "product": "MyLambda", - "packageType": "image", // "zip" | "image" - "architecture": "arm64", // x64 | arm64 - "containerCLI": "container", // docker | container | null (for zip) - "zipPath": null, // set for packageType=zip - "imageReference": "486652...dkr.ecr.eu-central-1.amazonaws.com/myrepo@sha256:", - "imageTag": "myrepo:latest" // informational -} -``` -- `ZipArchiveBackend` writes the manifest with `packageType: zip`, `zipPath`. **Backwards - compatible**: if no manifest is found, deploy falls back to today's path convention + - `.zip` assumption, so existing flows and the legacy `archive` plugin still work. -- `OCIArchiveBackend` writes `packageType: image` + `imageReference` (the already - index-unwrapped `@digest` — see findings) + `containerCLI` + `architecture`. -- `lambda-deploy` reads the manifest first; `--input-directory` still overrides the - lookup root. Deploy branches on `packageType`: zip → current flow; image → ECR/Image - flow, using `imageReference` directly (no re-resolution needed if build already - unwrapped the digest). `--cross-compile` on deploy defaults from `containerCLI`. -- Keeps the plugins thin: the manifest is produced/consumed by the **helper**, not the - SwiftPM plugins. This also finally makes the build→deploy contract explicit instead of - a hardcoded path string duplicated on both sides. - -RESOLVED — `login`/ECR-`tag`/`push` happen at **deploy** time, not build. Decided by the -plugins' sandbox permissions + credential split (verified in `Package@swift-6.4.swift`): -- `lambda-build` network scope = `.docker` (Docker socket only); it never touches AWS - credentials/STS. It is intentionally **offline-except-Docker**. -- `lambda-deploy` network scope = `.all(ports: [443])`; it already resolves AWS account - via STS and calls AWS APIs. - -ECR `login`+`push` need HTTPS-to-ECR (443) **and** AWS creds **and** region/account — all -deploy-only. Pushing from build would require widening build's sandbox to 443 + giving it -AWS creds, breaking the clean split. The correct analogy to ZIP: build *produces a local -artifact*, deploy *uploads it* (zip → S3/direct upload is already a deploy step; image → -ECR push is the same role). - -Step ownership: -- **build**: `build` the image + a **local** `tag` (`swift-lambda/:latest`, no - creds). Writes manifest with `packageType: image`, local `imageTag`, `containerCLI`, - `architecture`. No `imageReference` yet, no push. -- **deploy**: ensure ECR repo → `get-login-password`→`registry login` → ECR-qualified - re-`tag` (`.dkr.ecr.…`, needs account+region) → `push` → unwrap index to - child `@digest` → `create/update-function`. - -Consequence to document: `lambda-build --archive-format oci` alone does NOT push — the -image reaches ECR only on `lambda-deploy` (symmetric with zip: build makes it, deploy -uploads it). A build-time `--push` is intentionally NOT added (would force widening -build's sandbox + creds); revisit only on concrete CI demand. - -### D3. Cross-architecture build/deploy (lower priority) - -Today: `lambda-build` has **no `--architecture` flag** — the cross-compile container runs -under the host/daemon's default arch, and the produced binary's arch is whatever the -base image resolves to. `lambda-deploy` *does* have `--architecture` (default -`Architecture.host` via `#if arch`), passed to `CreateFunction.architectures`. So a -mismatch is already possible today (build arm64, deploy declares x64) and simply produces -a broken function — it's just latent. - -For correctness (and required for OCI, where image arch is baked in): -- Add `--architecture ` to **`lambda-build`** too, default host. Thread it into - the container build: docker `--platform linux/`, container `--arch ` - (verified `container` accepts `--arch`/`--platform`). The base `swift:*-amazonlinux2023` - is multi-arch so cross-building works via the daemon's emulation (qemu / Rosetta-backed - VM); note this is slow and may need the user's runtime to support emulation. -- Record `architecture` in the build manifest (D2); `lambda-deploy` **defaults its - `--architecture` from the manifest** instead of host, removing the latent mismatch. - Explicit `--architecture` on either side overrides; deploy should **error if its - declared arch ≠ the manifest arch** (can't deploy an arm64 image as x64). -- OCI specific: the function `--architectures` MUST match the image's single arch; since - `container` bakes one arch into the (unwrapped) child manifest, deploy derives it from - the manifest rather than guessing. -- Emulation availability (building x64 on an arm64 Mac and vice-versa) is environment - dependent; out of scope to guarantee — document it and surface the underlying CLI error - if emulation isn't set up. - -## Open questions for sign-off -- Q1. Artifact model: confirm the `enum Artifact` approach (PR-A item 3) vs keeping - `[String: URL]` and writing a `docker save` tarball. Plan assumes the enum. -- Q2. RESOLVED by dry-run (2026-06-29): the full soto-codegenerator output ALREADY - contains every OCI field — `FunctionCode.imageUri`, `UpdateFunctionCodeRequest.imageUri`, - and optional `runtime`/`handler` on `CreateFunctionRequest`. So image support is a pure - REGENERATION via the script, no hand-edits and no post-processing hack. BUT see Q5. -- Q5. NEW (blocker found in dry-run): `scripts/generate-aws-clients.sh` is out of date - with the current soto-codegenerator and will NOT reproduce the committed output as-is: - * CLI flags changed: now `--input-file` / `--output-folder` / `--config ` - (the script's `--model-path` / `--operations` / `--module` no longer apply; - `--operations` is ignored). - * Operation names in the config must be **camelCase** (`createFunction`), keyed by - the **model filename** (`lambda`). PascalCase matches nothing and emits only the - error type — likely how the committed clients ended up trimmed/ZIP-only. - → The script must be fixed (flags + camelCase config + filename keys) BEFORE or AS PART - OF the ECR/Lambda regen. This is itself a small standalone PR worth doing first: - "fix generate-aws-clients.sh for current soto-codegenerator" — re-running it should - then regenerate IAM/Lambda/S3/STS cleanly (and pick up the image fields for Lambda). -- Q3. One combined PR or the A/B split above? Recommend split — PR-A is shippable and - testable without any AWS account; PR-B needs ECR + real push verification. -- Q4. Scope of ECR ops: is `DescribeRepositories`+`CreateRepository`+`Get/SetRepositoryPolicy` - +`GetAuthorizationToken` the right minimal set, or also `CreateRepository` idempotency - via `RepositoryAlreadyExistsException` handling (like the S3 bucket path)? - -## Verified end-to-end with Apple `container` (2026-06-29, account 486652066693, eu-central-1) - -Built a minimal `FROM public.ecr.aws/lambda/provided:al2023` image with Apple -`container` (docker daemon was DOWN), pushed to ECR, created a Lambda function. -Findings: - -- **`container build` needs an explicit context dir** — the default `.` misbehaved - ("transferring context: 2B", COPY couldn't find files). Always pass `` - and `-f ` explicitly. -- **ECR login works identically to docker**: `aws ecr get-login-password | container - registry login --username AWS --password-stdin ` → "Login succeeded". -- **`container image tag` / `push` work** against ECR (39.6 MB pushed fine). -- **🚫 BLOCKER: `container` always exports an OCI image *index*** manifest - (`application/vnd.oci.image.index.v1+json`), even with `--arch arm64` / - `--platform linux/arm64` on build AND push. The index wraps a single linux/arm64 - child manifest (`application/vnd.oci.image.manifest.v1+json`). -- **Lambda REJECTS the index**: `create-function` with the index tag fails with - `InvalidParameterValueException: The image manifest, config or layer media type ... - is not supported`. (Lambda does not accept multi-arch / index manifests.) -- **✅ WORKAROUND (proven):** point Lambda at the **child manifest by digest**, not the - index tag: `ImageUri = @sha256:`. `create-function` then - succeeds (State: Pending → Active). Get the child digest from the pushed index via - `ecr batch-get-image ... imageTag=latest` → `imageManifest.manifests[0].digest`. - -**Design consequence:** the OCI deploy path must, after push, resolve the index to its -single child manifest digest and deploy *that* `@digest` as the `ImageUri` — not the -tag. (Alternatively, find a way to make `container` push a flat manifest; none of the -build/push `--arch`/`--platform` flags did so in testing. Digest-of-child is the -reliable path today.) For docker, a normal `docker build`/`push` produces a flat -`vnd.oci.image.manifest`/`docker v2s2` manifest that Lambda accepts directly by tag — -so this index-unwrap step is **container-specific** and belongs in `AppleContainerCLI` -or the deploy logic, gated on which CLI produced the image. - -## Risks -- ~~Apple `container` ECR login/push parity~~ — VERIFIED working (see above). The real - issue turned out to be the OCI-index manifest, with a proven digest-based workaround. -- Lambda client regeneration could surface other trimmed-shape gaps; keep the diff - reviewable. -- `provided:al2023` base image + single-arch requirement: the built binary's arch must - match the function arch and the base image arch — thread `--architecture` through. -- Image optimization latency: Image functions go Pending→Active after push; deploy - success reporting should account for it (poll state, like a future enhancement). From 57f4addae4961fee284656a6681ca07219de9423 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 29 Jun 2026 15:11:43 +0200 Subject: [PATCH 4/4] fix concurrency errors on 6.4 --- .../AWSLambdaPluginHelper.swift | 21 ++-------- .../StandardOutput.swift | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 Sources/AWSLambdaPluginHelper/StandardOutput.swift diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift index 6cd64346..ea2a8b09 100644 --- a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift @@ -13,20 +13,6 @@ // //===----------------------------------------------------------------------===// -// `@preconcurrency` suppresses the Swift 6 strict-concurrency diagnostic for the libc `stdout` -// global, which is an `extern FILE *` (a mutable global var) on Glibc/Musl. -#if os(macOS) -@preconcurrency import Darwin.C -#elseif canImport(Glibc) -@preconcurrency import Glibc -#elseif canImport(Musl) -@preconcurrency import Musl -#elseif os(Windows) -@preconcurrency import ucrt -#else -#error("Unsupported platform") -#endif - @main @available(LambdaSwift 2.0, *) struct AWSLambdaPluginHelper { @@ -38,10 +24,9 @@ struct AWSLambdaPluginHelper { } public static func main() async throws { - // SwiftPM runs plugins with stdout connected to a pipe rather than a TTY, so the C runtime - // block-buffers stdout and the helper's output (and --help text) only appears when the process - // exits. Force line buffering so each printed line streams to the user as it is produced. - setvbuf(stdout, nil, _IOLBF, 0) + // Stream output line-by-line; SwiftPM connects the plugin's stdout to a pipe, which the C + // runtime would otherwise block-buffer until the process exits. + enableLineBufferedStdout() let args = CommandLine.arguments let helper = AWSLambdaPluginHelper() diff --git a/Sources/AWSLambdaPluginHelper/StandardOutput.swift b/Sources/AWSLambdaPluginHelper/StandardOutput.swift new file mode 100644 index 00000000..e0ae1f81 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/StandardOutput.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// `stdout` is a libc global `var` (an `extern FILE *`) and so reading it trips the Swift 6 +// strict-concurrency check. `@preconcurrency` silences that diagnostic for libc symbols. It is +// confined to this dedicated file — the rest of the target imports libc normally and keeps full +// concurrency checking — to keep the suppression's blast radius as small as possible. +#if os(macOS) +@preconcurrency import Darwin.C +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif os(Windows) +@preconcurrency import ucrt +#else +#error("Unsupported platform") +#endif + +/// Switches `stdout` to line buffering. +/// +/// SwiftPM runs plugins with stdout connected to a pipe rather than a TTY, so the C runtime +/// block-buffers stdout and the helper's output (and `--help` text) only appears once the process +/// exits. Line buffering makes each printed line stream to the user as it is produced. +/// +/// Must be called once, before any output is produced. +func enableLineBufferedStdout() { + setvbuf(stdout, nil, _IOLBF, 0) +}