diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 148e9b1..f30dc68 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -14,11 +14,9 @@ concurrency: jobs: swift-test: name: Swift tests - runs-on: ubuntu-24.04-arm # free arm64 runner (public repo); matches prod arch - container: swift:6.3-amazonlinux2 # exact prod toolchain — same image scripts/build.sh uses + runs-on: ubuntu-24.04-arm # free arm64 runner (public repo); matches prod arch + container: swift:6.3-amazonlinux2023 # exact prod toolchain — same image scripts/build.sh uses; already bundles git + openssl-devel steps: - - name: Install git + openssl (AL2 image, needed by SwiftPM/deps) - run: yum -y install git openssl openssl-devel - uses: actions/checkout@v4 - name: swift test run: swift test diff --git a/Makefile b/Makefile index 8f42f4c..6cb3512 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -# SAM custom-runtime build target for the Swift Lambda (provided.al2). +# SAM custom-runtime build target for the Swift Lambda (provided.al2023). # `sam build` invokes `make build-NZImageApiFunction` with ARTIFACTS_DIR set; it then # zips ARTIFACTS_DIR itself (no package.sh needed). scripts/build.sh cross-compiles a -# static arm64 Amazon Linux 2 binary in Docker (pinned --platform linux/arm64 to match +# static arm64 Amazon Linux 2023 binary in Docker (pinned --platform linux/arm64 to match # Architectures: [arm64] in template.yaml). build-NZImageApiFunction: ./scripts/build.sh diff --git a/README.md b/README.md index ace31cb..fe8a2f3 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ The deployment script handles building with Docker and packaging with UPX: **Manual build steps:** ```bash -# 1. Build using Docker (compiles for Amazon Linux 2) +# 1. Build using Docker (compiles for Amazon Linux 2023) ./scripts/build.sh # 2. Package with UPX compression @@ -269,7 +269,7 @@ The deployment script handles building with Docker and packaging with UPX: 2. Select your function (or create new) 3. Upload `.build/lambda/NZImageApiLambda/lambda.zip` 4. Configure: - - Runtime: `provided.al2` (Amazon Linux 2) + - Runtime: `provided.al2023` (Amazon Linux 2023) - Handler: `bootstrap` - Architecture: `arm64` - Timeout: 60 seconds @@ -292,7 +292,7 @@ aws lambda update-function-configuration \ ```bash aws lambda create-function \ --function-name NZImageApiLambda \ - --runtime provided.al2 \ + --runtime provided.al2023 \ --role YOUR_LAMBDA_ROLE_ARN \ --handler bootstrap \ --zip-file fileb://.build/lambda/NZImageApiLambda/lambda.zip \ @@ -370,8 +370,8 @@ The design rationale (why free static secrets instead of Amazon Cognito) is reco ## Build Details The build process: -- Uses Docker with `swift:6.0-amazonlinux2` image -- Compiles for AWS Lambda's Amazon Linux 2 runtime +- Uses Docker with `swift:6.3-amazonlinux2023` image +- Compiles for AWS Lambda's Amazon Linux 2023 runtime - Statically links the Swift standard library - Compresses the binary with UPX (~70% size reduction: 106MB → 32MB) - Creates a `bootstrap` executable (required by AWS custom runtime) diff --git a/Sources/Testing/CollectionLister/main.swift b/Sources/Testing/CollectionLister/main.swift index 23a929c..eebf3a1 100644 --- a/Sources/Testing/CollectionLister/main.swift +++ b/Sources/Testing/CollectionLister/main.swift @@ -5,6 +5,14 @@ // Lists all image collections from Digital NZ and their counts // +// Must be the first import: Glibc's stdout/stderr aren't marked Sendable, so Swift 6 strict +// concurrency flags any reference to them. @preconcurrency suppresses that, but only if this +// import is resolved before Foundation's own (non-preconcurrency) transitive import of Glibc — +// see https://github.com/swiftlang/swift/issues/77866. +#if canImport(Glibc) +@preconcurrency import Glibc +#endif + import Alamofire import Foundation @@ -12,6 +20,10 @@ import Foundation import FoundationNetworking #endif +// nonisolated(unsafe) because this is a new global of a non-Sendable type +// (UnsafeMutablePointer); safe here since this CLI is single-threaded terminal I/O. +nonisolated(unsafe) private let standardError = stderr + // MARK: - Models struct FacetsResponse: Codable { @@ -47,7 +59,7 @@ struct CollectionListOutput: Codable { struct CollectionListerApp { static func main() async throws { guard let apiKey = ProcessInfo.processInfo.environment["DIGITALNZ_API_KEY"] else { - fputs("Error: DIGITALNZ_API_KEY environment variable not set\n", stderr) + fputs("Error: DIGITALNZ_API_KEY environment variable not set\n", standardError) exit(1) } @@ -75,12 +87,12 @@ struct CollectionListerApp { // Check for API errors if let errors = response.errors, !errors.isEmpty { - fputs("API returned errors: \(errors.joined(separator: ", "))\n", stderr) + fputs("API returned errors: \(errors.joined(separator: ", "))\n", standardError) exit(1) } guard let facetDict = response.search?.facets?.primaryCollection else { - fputs("Error: No facet data returned from API\n", stderr) + fputs("Error: No facet data returned from API\n", standardError) exit(1) } diff --git a/Sources/Testing/CollectionReviewer/main.swift b/Sources/Testing/CollectionReviewer/main.swift index a18fc8d..b1797ab 100644 --- a/Sources/Testing/CollectionReviewer/main.swift +++ b/Sources/Testing/CollectionReviewer/main.swift @@ -5,6 +5,14 @@ // Interactive review tool for all collections // +// Must be the first import: Glibc's stdout/stderr aren't marked Sendable, so Swift 6 strict +// concurrency flags any reference to them. @preconcurrency suppresses that, but only if this +// import is resolved before Foundation's own (non-preconcurrency) transitive import of Glibc — +// see https://github.com/swiftlang/swift/issues/77866. +#if canImport(Glibc) +@preconcurrency import Glibc +#endif + import Foundation import LambdaTesting import Synchronization @@ -13,6 +21,11 @@ import Synchronization import FoundationNetworking #endif +// nonisolated(unsafe) because these are new globals of a non-Sendable type +// (UnsafeMutablePointer); safe here since this CLI is single-threaded terminal I/O. +nonisolated(unsafe) private let standardOutput = stdout +nonisolated(unsafe) private let standardError = stderr + // MARK: - Progress Tracking struct ProgressState: Sendable { @@ -229,7 +242,7 @@ func printClickableLink(_ url: URL, label: String) { func getSelectionInput(prompt: String) -> SelectionStatus { print(prompt, terminator: " ") - fflush(stdout) + fflush(standardOutput) while true { if let input = readLine()?.lowercased().trimmingCharacters(in: .whitespaces) { @@ -249,13 +262,13 @@ func getSelectionInput(prompt: String) -> SelectionStatus { } } print("Please enter y/n/u/m/s: ", terminator: "") - fflush(stdout) + fflush(standardOutput) } } func getTextInput(prompt: String) -> String { print(prompt, terminator: " ") - fflush(stdout) + fflush(standardOutput) return readLine() ?? "" } @@ -297,7 +310,7 @@ struct CollectionReviewerApp { progressState.withLock { $0.filePath = "\(currentDir)/Research/.collection-reviewer-progress" } guard fileManager.fileExists(atPath: detailsPath) else { - fputs("Error: Could not find \(detailsPath)\n", stderr) + fputs("Error: Could not find \(detailsPath)\n", standardError) exit(1) } diff --git a/Sources/Testing/ImageResolutionChecker/main.swift b/Sources/Testing/ImageResolutionChecker/main.swift index 391b65d..314e54b 100644 --- a/Sources/Testing/ImageResolutionChecker/main.swift +++ b/Sources/Testing/ImageResolutionChecker/main.swift @@ -5,9 +5,19 @@ // Checks image resolutions for Digital NZ collections // +// Must be the first import: Glibc's stdout/stderr aren't marked Sendable, so Swift 6 strict +// concurrency flags any reference to them. @preconcurrency suppresses that, but only if this +// import is resolved before Foundation's own (non-preconcurrency) transitive import of Glibc — +// see https://github.com/swiftlang/swift/issues/77866. +#if canImport(Glibc) +@preconcurrency import Glibc +#endif + import Alamofire import Foundation +#if canImport(ImageIO) import ImageIO +#endif import OrderedCollections import RichError @@ -15,6 +25,10 @@ import RichError import FoundationNetworking #endif +// nonisolated(unsafe) because this is a new global of a non-Sendable type +// (UnsafeMutablePointer); safe here since this CLI is single-threaded terminal I/O. +nonisolated(unsafe) private let standardError = stderr + // MARK: - Models struct NZRecordsResult: Codable, Sendable { @@ -79,6 +93,7 @@ struct ResolutionCheckOutput: Codable { // MARK: - Image Resolution Checker func getImageResolution(from url: URL) async -> ImageResolution? { + #if canImport(ImageIO) do { let (data, _) = try await URLSession.shared.data(from: url) @@ -94,6 +109,11 @@ func getImageResolution(from url: URL) async -> ImageResolution? { } catch { return nil } + #else + // ImageIO is Darwin-only; this dev CLI tool isn't built/run on Linux (see CLAUDE.md's + // "Sources/Testing/*" CI exclusion), but Linux `swift test` still compiles this target. + return nil + #endif } func checkImageURL(_ url: URL?) async -> ImageCheckResult? { @@ -172,20 +192,20 @@ struct ImageResolutionCheckerApp { let arguments = CommandLine.arguments guard arguments.count >= 2 else { - fputs("Usage: ImageResolutionChecker \n", stderr) + fputs("Usage: ImageResolutionChecker \n", standardError) exit(1) } let collection = arguments[1] guard let apiKey = ProcessInfo.processInfo.environment["DIGITALNZ_API_KEY"] else { - fputs("Error: DIGITALNZ_API_KEY environment variable not set\n", stderr) + fputs("Error: DIGITALNZ_API_KEY environment variable not set\n", standardError) exit(1) } // Fetch a result from Digital NZ guard let result = try await fetchDigitalNZResult(collection: collection, apiKey: apiKey) else { - fputs("Error: No results found for collection '\(collection)'\n", stderr) + fputs("Error: No results found for collection '\(collection)'\n", standardError) exit(1) } diff --git a/Sources/Testing/LambdaTesting/LambdaTesting.swift b/Sources/Testing/LambdaTesting/LambdaTesting.swift index e585c6f..5e4d52b 100644 --- a/Sources/Testing/LambdaTesting/LambdaTesting.swift +++ b/Sources/Testing/LambdaTesting/LambdaTesting.swift @@ -11,6 +11,9 @@ import Darwin #else import Glibc #endif +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif // MARK: - Lambda Response diff --git a/Tests/NZImageApiLambdaTests/DigitalNZAPIDataSourcePageMathTests.swift b/Tests/NZImageApiLambdaTests/DigitalNZAPIDataSourcePageMathTests.swift index 9dfd91b..53c007c 100644 --- a/Tests/NZImageApiLambdaTests/DigitalNZAPIDataSourcePageMathTests.swift +++ b/Tests/NZImageApiLambdaTests/DigitalNZAPIDataSourcePageMathTests.swift @@ -8,6 +8,9 @@ import Foundation import XCTest @testable import NZImageApiLambda +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif /// Stands in for the real network-backed `ValidatedRequestManager`. Returns a canned /// `NZRecordsResponse` for both the initial (`per_page=0`, count-only) and secondary diff --git a/Tests/NZImageApiLambdaTests/NetworkRequestManagerValidationTests.swift b/Tests/NZImageApiLambdaTests/NetworkRequestManagerValidationTests.swift index fbc5e8d..d0f965d 100644 --- a/Tests/NZImageApiLambdaTests/NetworkRequestManagerValidationTests.swift +++ b/Tests/NZImageApiLambdaTests/NetworkRequestManagerValidationTests.swift @@ -6,8 +6,12 @@ // testable without any actual network request. // +import Foundation import XCTest @testable import NZImageApiLambda +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif final class NetworkRequestManagerValidationTests: XCTestCase { private func response(statusCode: Int, contentType: String?) -> HTTPURLResponse { diff --git a/scripts/build.sh b/scripts/build.sh index 3ded04c..b5c0a5b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -7,11 +7,11 @@ echo "Cleaning previous build artifacts..." rm -rf .build/checkouts .build/repositories .build/workspace-state.json # Build using Docker -echo "Building Lambda for Amazon Linux 2..." +echo "Building Lambda for Amazon Linux 2023..." docker run \ --rm \ --platform linux/arm64 \ --volume "$(pwd)/:/src" \ --workdir "/src/" \ - swift:6.3-amazonlinux2 \ - /bin/bash -c "yum -y update; yum -y install openssl openssl-devel -y; swift build --product NZImageApiLambda -c release -Xswiftc -static-stdlib" + swift:6.3-amazonlinux2023 \ + /bin/bash -c "dnf -y install openssl-devel; swift build --product NZImageApiLambda -c release -Xswiftc -static-stdlib" diff --git a/template.yaml b/template.yaml index d819918..1261c35 100644 --- a/template.yaml +++ b/template.yaml @@ -144,13 +144,13 @@ Resources: AlarmActions: - !Ref AlarmTopic - # --- Swift NZ Image API (provided.al2 custom runtime, built via the Makefile) ------- + # --- Swift NZ Image API (provided.al2023 custom runtime, built via the Makefile) ------- NZImageApiFunction: Type: AWS::Serverless::Function Properties: CodeUri: . Handler: bootstrap - Runtime: provided.al2 + Runtime: provided.al2023 Architectures: - arm64 MemorySize: 512