Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Plugins/AWSLambdaBuilder/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ struct AWSLambdaBuilder: CommandPlugin {
// The helper requires --configuration; the plugin supplies the default. Validation of the
// value itself is left to the helper.
let configurationArgument = argumentExtractor.extractOption(named: "configuration")
// `--container-cli` is a deprecated alias for `--cross-compile`.
let crossCompileArgument = argumentExtractor.extractOption(named: "cross-compile")
let containerCliArgument = argumentExtractor.extractOption(named: "container-cli")
// `--container-cli` only exists on the deprecated `archive` command. Reject it here rather
// than let it fall through to the helper and be silently ignored (which would build with the
// wrong CLI). The user is told to use the canonical `--cross-compile` instead.
guard argumentExtractor.extractOption(named: "container-cli").isEmpty else {
throw BuilderErrors.invalidArgument(
"'--container-cli' is not supported by lambda-build. Use '--cross-compile <docker|container>' instead."
)
}

// Resolve the container CLI that matches the requested cross-compilation method. The plugin
// sandbox can only run tools it resolves up front, so we must pick the right binary here:
// `container` for `--cross-compile container`, `docker` otherwise.
let crossCompileMethod = (crossCompileArgument.first ?? containerCliArgument.first)?.lowercased()
let crossCompileMethod = crossCompileArgument.first?.lowercased()
let containerCLIToolName = crossCompileMethod == "container" ? "container" : "docker"
let containerToolPath = try context.tool(named: containerCLIToolName).url
let zipToolPath = try context.tool(named: "zip").url
Expand Down
38 changes: 29 additions & 9 deletions Plugins/AWSLambdaDeployer/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ struct AWSLambdaDeployer: CommandPlugin {
var argumentExtractor = ArgumentExtractor(arguments)
let productsArgument = argumentExtractor.extractOption(named: "products")
// `--cross-compile` selects the container CLI used to push an OCI image to ECR (docker or
// container). The plugin sandbox can only run tools it resolves up front, so the matching
// binary is resolved here and forwarded as --cross-compile-tool-path. For a ZIP deploy this
// is unused; resolving docker by default is harmless.
// container).
let crossCompileArgument = argumentExtractor.extractOption(named: "cross-compile")
// `--container-cli` only exists on the deprecated `archive` command. Reject it here rather
// than let it fall through to the helper and be silently ignored (which would push with the
// wrong CLI). The user is told to use the canonical `--cross-compile` instead.
guard argumentExtractor.extractOption(named: "container-cli").isEmpty else {
throw DeployerPluginErrors.invalidArgument(
"'--container-cli' is not supported by lambda-deploy. Use '--cross-compile <docker|container>' instead."
)
}

let products: [Product]
if !productsArgument.isEmpty {
Expand All @@ -44,17 +50,20 @@ struct AWSLambdaDeployer: CommandPlugin {
let productNames = products.map { $0.name }.joined(separator: ",")

let crossCompile = crossCompileArgument.first?.lowercased()
let containerCLIToolName = crossCompile == "container" ? "container" : "docker"
// Best-effort: a container CLI may not be installed for a plain ZIP deploy, so don't fail
// here if it can't be resolved — the helper only needs it for an image artifact.
let containerToolPath = try? context.tool(named: containerCLIToolName).url

var args = ["deploy", "--products", productNames]
if let crossCompile {
args += ["--cross-compile", crossCompile]
}
if let containerToolPath {
args += ["--cross-compile-tool-path", containerToolPath.path]
// The CLI flavor to use is only known after the helper reads the build manifest, and the
// plugin sandbox can only run tools it resolves up front. So resolve every container CLI that
// is installed and forward each as `--cross-compile-tool-path <name>=<path>`; the helper then
// picks the path matching the CLI it selects. Best-effort: a plain ZIP deploy needs none, so
// a CLI that can't be resolved is simply omitted.
for cliName in ["docker", "container"] {
if let toolPath = try? context.tool(named: cliName).url {
args += ["--cross-compile-tool-path", "\(cliName)=\(toolPath.path)"]
}
}
args += argumentExtractor.remainingArguments

Expand All @@ -75,3 +84,14 @@ struct AWSLambdaDeployer: CommandPlugin {
}

}

private enum DeployerPluginErrors: Error, CustomStringConvertible {
case invalidArgument(String)

var description: String {
switch self {
case .invalidArgument(let description):
return description
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ struct ZipArchiveBackend: ArchiveBackend {
/// The resolved path to the `zip` tool.
let zipToolPath: URL

/// The architecture the binaries were built for, recorded in the build manifest so the deploy
/// step deploys the function for the architecture it was actually built for.
let architecture: BuildArchitecture

// TODO: explore using ziplib or similar instead of shelling out
func archive(
products: [String: URL],
Expand Down Expand Up @@ -121,7 +125,7 @@ struct ZipArchiveBackend: ArchiveBackend {
// (package type + architecture) instead of re-deriving everything from the path.
try BuildManifest.zip(
product: product,
architecture: .host,
architecture: self.architecture,
zipPath: zipfilePath.path()
).write(into: workingDirectory)

Expand Down
27 changes: 12 additions & 15 deletions Sources/AWSLambdaPluginHelper/lambda-build/BuildArchitecture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@

/// The target CPU architecture an artifact is built for.
///
/// An OCI image bakes in a single architecture, so the build step needs to know which one to
/// target. Today this defaults to the host architecture; a user-facing `--architecture` flag and
/// build-manifest plumbing are tracked separately (issue #683) and will set this explicitly.
/// Set explicitly by `--architecture` (defaulting to the host architecture) and recorded in the
/// build manifest so `lambda-deploy` deploys the function for the architecture the binary was
/// actually built for. Both the ZIP and OCI archive backends target this single architecture.
@available(LambdaSwift 2.0, *)
enum BuildArchitecture: String, Codable, CustomStringConvertible {
case x64
Expand All @@ -32,20 +32,17 @@ enum BuildArchitecture: String, Codable, CustomStringConvertible {
#endif
}

/// The value docker's `--platform` flag expects (`linux/amd64`, `linux/arm64`).
var dockerPlatform: String {
switch self {
case .x64: return "linux/amd64"
case .arm64: return "linux/arm64"
/// Parses the `--architecture` value, defaulting to the host architecture when omitted.
static func parse(_ value: String?) throws -> Self {
guard let value else {
return .host
}
}

/// The value Apple `container`'s `--arch` flag expects (`amd64`, `arm64`).
var containerArch: String {
switch self {
case .x64: return "amd64"
case .arm64: return "arm64"
guard let architecture = BuildArchitecture(rawValue: value.lowercased()) else {
throw BuilderErrors.invalidArgument(
"invalid architecture '\(value)'. Use 'x64' or 'arm64'."
)
}
return architecture
}

var description: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,29 @@
struct AppleContainerCLI: ContainerCLI {
let executableName = "container"

func pullArguments(image: String) -> [String] {
["image", "pull", image]
/// The value Apple `container`'s `--arch` flag expects (`amd64`, `arm64`).
func arch(for architecture: BuildArchitecture) -> String {
switch architecture {
case .x64: return "amd64"
case .arm64: return "arm64"
}
}

func pullArguments(image: String, architecture: BuildArchitecture) -> [String] {
["image", "pull", "--platform", "linux/\(self.arch(for: architecture))", image]
}

func runArguments(
baseImage: String,
architecture: BuildArchitecture,
workingDirectory: String,
mounts: [String],
env: [String: String]?,
command: String
) -> [String] {
// container's runtime needs a bit more memory than the default
var args: [String] = ["run", "--memory", "4G", "--rm"]
// container's runtime needs a bit more memory than the default. `--arch` selects the
// container's CPU architecture, which is what the in-container `swift build` compiles for.
var args: [String] = ["run", "--arch", self.arch(for: architecture), "--memory", "4G", "--rm"]
for mount in mounts {
args += ["-v", mount]
}
Expand All @@ -59,7 +69,7 @@ struct AppleContainerCLI: ContainerCLI {
// explicit `-f`. `--arch` selects the single target architecture.
[
"build",
"--arch", architecture.containerArch,
"--arch", self.arch(for: architecture),
"-f", dockerfile,
"-t", tag,
contextDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ struct ContainerBuildBackend: BuildBackend {
let baseImage: String
let disableImageUpdate: Bool

/// The CPU architecture to build for. The container runs as this architecture, so the compiled
/// binary targets it; recorded in the build manifest and matched against the deploy architecture.
let architecture: BuildArchitecture

/// The cross-compile method that selected this backend, retained for error reporting.
let method: CrossCompileMethod

Expand Down Expand Up @@ -60,7 +64,7 @@ struct ContainerBuildBackend: BuildBackend {
print("updating \"\(self.baseImage)\" image")
try Utils.execute(
executable: self.toolPath,
arguments: self.cli.pullArguments(image: self.baseImage),
arguments: self.cli.pullArguments(image: self.baseImage, architecture: self.architecture),
logLevel: verboseLogging ? .debug : .output
)
}
Expand All @@ -71,6 +75,7 @@ struct ContainerBuildBackend: BuildBackend {
executable: self.toolPath,
arguments: self.cli.runArguments(
baseImage: self.baseImage,
architecture: self.architecture,
workingDirectory: "/workspace",
mounts: ["\(packageDirectory.path()):/workspace"],
env: nil,
Expand Down Expand Up @@ -103,6 +108,7 @@ struct ContainerBuildBackend: BuildBackend {
executable: self.toolPath,
arguments: self.cli.runArguments(
baseImage: self.baseImage,
architecture: self.architecture,
workingDirectory: "/workspace/\(slice.joined(separator: "/"))",
mounts: ["\(packageDirectory.path())../..:/workspace"],
env: ["LAMBDA_USE_LOCAL_DEPS": localPath],
Expand All @@ -115,6 +121,7 @@ struct ContainerBuildBackend: BuildBackend {
executable: self.toolPath,
arguments: self.cli.runArguments(
baseImage: self.baseImage,
architecture: self.architecture,
workingDirectory: "/workspace",
mounts: ["\(packageDirectory.path()):/workspace"],
env: nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,23 @@ protocol ContainerCLI {
/// The name of the executable to resolve and run (e.g. "docker", "container").
var executableName: String { get }

/// The arguments to pull (update) the given base image.
func pullArguments(image: String) -> [String]
/// The arguments to pull (update) the given base image for a target architecture.
func pullArguments(image: String, architecture: BuildArchitecture) -> [String]

/// The arguments to run a command inside a container created from `baseImage`.
///
/// - Parameters:
/// - baseImage: The container image to run.
/// - architecture: The CPU architecture the container should run as. The build compiles for
/// the container's architecture, so this is what determines the produced binary's
/// architecture and must match what the function is deployed for.
/// - workingDirectory: The working directory inside the container.
/// - mounts: Volume mounts, each in the CLI's `host:container` form.
/// - env: Environment variables to set inside the container, or `nil`.
/// - command: The shell command to execute inside the container.
func runArguments(
baseImage: String,
architecture: BuildArchitecture,
workingDirectory: String,
mounts: [String],
env: [String: String]?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,27 @@
struct DockerCLI: ContainerCLI {
let executableName = "docker"

func pullArguments(image: String) -> [String] {
["pull", image]
/// The value docker's `--platform` flag expects (`linux/amd64`, `linux/arm64`).
func platform(for architecture: BuildArchitecture) -> String {
switch architecture {
case .x64: return "linux/amd64"
case .arm64: return "linux/arm64"
}
}

func pullArguments(image: String, architecture: BuildArchitecture) -> [String] {
["pull", "--platform", self.platform(for: architecture), image]
}

func runArguments(
baseImage: String,
architecture: BuildArchitecture,
workingDirectory: String,
mounts: [String],
env: [String: String]?,
command: String
) -> [String] {
var args: [String] = ["run", "--rm"]
var args: [String] = ["run", "--platform", self.platform(for: architecture), "--rm"]
for mount in mounts {
args += ["-v", mount]
}
Expand All @@ -53,7 +62,7 @@ struct DockerCLI: ContainerCLI {
) -> [String] {
[
"build",
"--platform", architecture.dockerPlatform,
"--platform", self.platform(for: architecture),
"-f", dockerfile,
"-t", tag,
contextDir,
Expand Down
Loading