Skip to content

Commit 06191b2

Browse files
authored
[plugin] Refactor lambda-build packaging into a pluggable ArchiveBackend (ZIP today, OCI-ready) (#681)
### Summary Introduces a pluggable **archive backend** abstraction for `lambda-build`, mirroring the existing build-backend design (#679). Packaging the built binaries into a deployable artifact was hard-coded to ZIP inside `Builder.swift`; it is now behind an `ArchiveBackend` protocol with one implementation today (ZIP, the existing code moved verbatim) and a stubbed seam for OCI images in the future. Internal refactor: the default behaviour is unchanged (ZIP), no public Swift API changes, and the output ZIP layout / path is identical. ### What changed - **`ArchiveBackend` protocol** (`ArchiveBackends/ArchiveBackend.swift`) — owns *what kind of artifact* is produced from the built binaries. It's the packaging counterpart to `BuildBackend` (which owns *how the binaries are built*); `Builder` selects and sequences the two. - **`ZipArchiveBackend`** (`ArchiveBackends/ZipArchiveBackend.swift`) — the existing `Builder.package(...)` body, moved unchanged, with `zipToolPath` injected via init. - **`ArchiveFormat` enum** (`ArchiveFormat.swift`) — the parsed `--archive-format` value (`zip` | `oci`), mirroring `CrossCompileMethod`: `parse(...)` (defaults to `zip`, case-insensitive), `isSupported` (`oci` → false), and a new `BuilderErrors.unsupportedArchiveFormat` case. - **`--archive-format zip|oci` flag** — parsed into `BuilderConfiguration`, with a `makeArchiveBackend()` factory (following the same on-configuration factory convention as `makeCrossCompileBackend()`). `oci` is recognised but throws a "not yet supported" error. Help text and the verbose config dump updated. - **`Builder.build(...)`** now ends with `configuration.makeArchiveBackend().archive(...)` instead of the inline `package(...)` method (which is removed). - **Directory rename**: `Backends/` → `BuildBackends/`, so the two families read as parallel siblings: `BuildBackends/` and `ArchiveBackends/`. ### Adding OCI later Add `OCIArchiveBackend: ArchiveBackend` under `ArchiveBackends/`, then flip the `oci` case in `ArchiveFormat.isSupported` and `makeArchiveBackend()`. No other changes. ### Testing - New `ArchiveBackendTests.swift`: `ArchiveFormat.parse` (default/case-insensitive/ oci-unsupported/unknown), `makeArchiveBackend()` selection (zip → `ZipArchiveBackend`), and `ZipArchiveBackend.archive` run against a temp build dir asserting the `.zip` and relocated `bootstrap` are produced. - All `AWSLambdaPluginHelperTests` pass (90 tests); `swift build` and `swift format lint --strict` clean. - End-to-end: `--archive-format oci` fails fast with the stub error; zip/default produce a byte-for-byte identical artifact to before.
1 parent d5a3bf0 commit 06191b2

11 files changed

Lines changed: 378 additions & 98 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright SwiftAWSLambdaRuntime project authors
6+
// Copyright (c) Amazon.com, Inc. or its affiliates.
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+
#if canImport(FoundationEssentials)
17+
import FoundationEssentials
18+
#else
19+
import Foundation
20+
#endif
21+
22+
/// A strategy for packaging built Lambda executables into a deployable artifact.
23+
///
24+
/// An archive backend owns *what kind of artifact* is produced from the built binaries — today a
25+
/// ZIP package suitable for upload to AWS Lambda, and in the future other formats such as an OCI
26+
/// image. It is the packaging counterpart to ``BuildBackend`` (which owns *how the binaries are
27+
/// built*); the two are selected and sequenced by ``Builder``.
28+
@available(LambdaSwift 2.0, *)
29+
protocol ArchiveBackend {
30+
/// A human-readable name used in log output (e.g. "zip").
31+
var name: String { get }
32+
33+
/// Package the built product executables into deployable artifacts.
34+
///
35+
/// - Parameters:
36+
/// - products: A map of product name to the built executable's URL on the host, as returned
37+
/// by a ``BuildBackend``.
38+
/// - outputDirectory: The directory where the artifacts should be written.
39+
/// - verboseLogging: When `true`, emit verbose output for debugging.
40+
/// - Returns: A map of product name to the produced artifact's URL.
41+
func archive(
42+
products: [String: URL],
43+
outputDirectory: URL,
44+
verboseLogging: Bool
45+
) throws -> [String: URL]
46+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright SwiftAWSLambdaRuntime project authors
6+
// Copyright (c) Amazon.com, Inc. or its affiliates.
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+
#if canImport(FoundationEssentials)
17+
import FoundationEssentials
18+
#else
19+
import Foundation
20+
#endif
21+
22+
/// Packages built executables into ZIP archives suitable for upload to AWS Lambda.
23+
///
24+
/// Each product is laid out as a `bootstrap` executable (the name the Lambda runtime expects)
25+
/// alongside any `*.resources` bundles, then zipped via the `zip` tool.
26+
@available(LambdaSwift 2.0, *)
27+
struct ZipArchiveBackend: ArchiveBackend {
28+
let name = "zip"
29+
30+
/// The resolved path to the `zip` tool.
31+
let zipToolPath: URL
32+
33+
// TODO: explore using ziplib or similar instead of shelling out
34+
func archive(
35+
products: [String: URL],
36+
outputDirectory: URL,
37+
verboseLogging: Bool
38+
) throws -> [String: URL] {
39+
40+
var archives = [String: URL]()
41+
for (product, artifactPath) in products {
42+
print("-------------------------------------------------------------------------")
43+
print("archiving \"\(product)\"")
44+
print("-------------------------------------------------------------------------")
45+
46+
// prep zipfile location
47+
let workingDirectory = outputDirectory.appending(path: product)
48+
let zipfilePath = workingDirectory.appending(path: "\(product).zip")
49+
if FileManager.default.fileExists(atPath: workingDirectory.path()) {
50+
try FileManager.default.removeItem(atPath: workingDirectory.path())
51+
}
52+
try FileManager.default.createDirectory(atPath: workingDirectory.path(), withIntermediateDirectories: true)
53+
54+
// rename artifact to "bootstrap"
55+
let relocatedArtifactPath = workingDirectory.appending(path: "bootstrap")
56+
try FileManager.default.copyItem(atPath: artifactPath.path(), toPath: relocatedArtifactPath.path())
57+
58+
var arguments: [String] = []
59+
#if os(macOS) || os(Linux)
60+
arguments = [
61+
"--recurse-paths",
62+
"--symlinks",
63+
zipfilePath.lastPathComponent,
64+
relocatedArtifactPath.lastPathComponent,
65+
]
66+
#else
67+
throw BuilderErrors.unsupportedPlatform("can't or don't know how to create a zip file on this platform")
68+
#endif
69+
70+
// add resources
71+
var artifactPathComponents = artifactPath.pathComponents
72+
_ = artifactPathComponents.removeFirst() // Get rid of beginning "/"
73+
_ = artifactPathComponents.removeLast() // Get rid of the name of the package
74+
let artifactDirectory = "/\(artifactPathComponents.joined(separator: "/"))"
75+
for fileInArtifactDirectory in try FileManager.default.contentsOfDirectory(atPath: artifactDirectory) {
76+
guard let artifactURL = URL(string: "\(artifactDirectory)/\(fileInArtifactDirectory)") else {
77+
continue
78+
}
79+
80+
guard artifactURL.pathExtension == "resources" else {
81+
continue // Not resources, so don't copy
82+
}
83+
let resourcesDirectoryName = artifactURL.lastPathComponent
84+
let relocatedResourcesDirectory = workingDirectory.appending(path: resourcesDirectoryName)
85+
if FileManager.default.fileExists(atPath: artifactURL.path()) {
86+
do {
87+
arguments.append(resourcesDirectoryName)
88+
try FileManager.default.copyItem(
89+
atPath: artifactURL.path(),
90+
toPath: relocatedResourcesDirectory.path()
91+
)
92+
} catch let error as CocoaError {
93+
94+
// On Linux, when the build has been done with Docker,
95+
// the source file are owned by root
96+
// this causes a permission error **after** the files have been copied
97+
// see https://github.com/awslabs/swift-aws-lambda-runtime/issues/449
98+
// see https://forums.swift.org/t/filemanager-copyitem-on-linux-fails-after-copying-the-files/77282
99+
100+
// because this error happens after the files have been copied, we can ignore it
101+
// this code checks if the destination file exists
102+
// if they do, just ignore error, otherwise throw it up to the caller.
103+
if !(error.code == CocoaError.Code.fileWriteNoPermission
104+
&& FileManager.default.fileExists(atPath: relocatedResourcesDirectory.path()))
105+
{
106+
throw error
107+
} // else just ignore it
108+
}
109+
}
110+
}
111+
112+
// run the zip tool
113+
try Utils.execute(
114+
executable: self.zipToolPath,
115+
arguments: arguments,
116+
customWorkingDirectory: workingDirectory,
117+
logLevel: verboseLogging ? .debug : .silent
118+
)
119+
120+
archives[product] = zipfilePath
121+
}
122+
return archives
123+
}
124+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright SwiftAWSLambdaRuntime project authors
6+
// Copyright (c) Amazon.com, Inc. or its affiliates.
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+
#if canImport(FoundationEssentials)
17+
import FoundationEssentials
18+
#else
19+
import Foundation
20+
#endif
21+
22+
/// The packaging format requested via `--archive-format`.
23+
///
24+
/// This enum is purely the parsed user choice. The packaging flow lives in the ``ArchiveBackend``
25+
/// types, and backend construction lives on ``BuilderConfiguration/makeArchiveBackend()``.
26+
@available(LambdaSwift 2.0, *)
27+
enum ArchiveFormat: String, CustomStringConvertible {
28+
/// A ZIP package suitable for upload to AWS Lambda.
29+
case zip
30+
/// An OCI image. Not yet supported.
31+
case oci
32+
33+
var isSupported: Bool {
34+
switch self {
35+
case .zip: return true
36+
case .oci: return false
37+
}
38+
}
39+
40+
static func parse(_ value: String?) throws -> Self {
41+
guard let value else {
42+
return .zip
43+
}
44+
45+
guard let format = ArchiveFormat(rawValue: value.lowercased()) else {
46+
throw BuilderErrors.invalidArgument(
47+
"invalid archive format '\(value)'. Use 'zip' or 'oci'."
48+
)
49+
}
50+
51+
guard format.isSupported else {
52+
throw BuilderErrors.unsupportedArchiveFormat(format)
53+
}
54+
55+
return format
56+
}
57+
58+
var description: String {
59+
self.rawValue
60+
}
61+
}

Sources/AWSLambdaPluginHelper/lambda-build/Backends/AppleContainerCLI.swift renamed to Sources/AWSLambdaPluginHelper/lambda-build/BuildBackends/AppleContainerCLI.swift

File renamed without changes.

Sources/AWSLambdaPluginHelper/lambda-build/Backends/BuildBackend.swift renamed to Sources/AWSLambdaPluginHelper/lambda-build/BuildBackends/BuildBackend.swift

File renamed without changes.

Sources/AWSLambdaPluginHelper/lambda-build/Backends/ContainerBuildBackend.swift renamed to Sources/AWSLambdaPluginHelper/lambda-build/BuildBackends/ContainerBuildBackend.swift

File renamed without changes.

Sources/AWSLambdaPluginHelper/lambda-build/Backends/ContainerCLI.swift renamed to Sources/AWSLambdaPluginHelper/lambda-build/BuildBackends/ContainerCLI.swift

File renamed without changes.

Sources/AWSLambdaPluginHelper/lambda-build/Backends/DockerCLI.swift renamed to Sources/AWSLambdaPluginHelper/lambda-build/BuildBackends/DockerCLI.swift

File renamed without changes.

Sources/AWSLambdaPluginHelper/lambda-build/Backends/NativeBuildBackend.swift renamed to Sources/AWSLambdaPluginHelper/lambda-build/BuildBackends/NativeBuildBackend.swift

File renamed without changes.

0 commit comments

Comments
 (0)