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
43 changes: 38 additions & 5 deletions Plugins/AWSLambdaBuilder/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,45 @@ struct AWSLambdaBuilder: CommandPlugin {
)
}

// 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:
// Resolve the tool 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:
// `swift` for `--cross-compile swift-static-sdk` (no container runtime needed),
// `container` for `--cross-compile container`, `docker` otherwise.
let crossCompileMethod = crossCompileArgument.first?.lowercased()
let containerCLIToolName = crossCompileMethod == "container" ? "container" : "docker"
let containerToolPath = try context.tool(named: containerCLIToolName).url
let crossCompileToolName: String
switch crossCompileMethod {
case "swift-static-sdk":
crossCompileToolName = "swift"
// The Static Linux SDK builds without a container, so the docker/container-specific
// options do not apply. Reject them here rather than let them be silently ignored.
// These flags are forwarded verbatim to the helper, so inspect the raw arguments.
let incompatibleWithStaticSDK = [
"--base-docker-image",
"--swift-version",
"--disable-docker-image-update",
"--base-oci-image",
]
for flag in incompatibleWithStaticSDK where arguments.contains(flag) {
throw BuilderErrors.invalidArgument(
"'\(flag)' cannot be used with '--cross-compile swift-static-sdk'; it targets a "
+ "container-based build. Remove it, or choose '--cross-compile docker' or 'container'."
)
}
// The OCI image build requires a container CLI, so it is incompatible too. Match the
// value that follows --archive-format rather than a bare "oci" token anywhere.
if let formatIndex = arguments.firstIndex(of: "--archive-format"),
arguments.indices.contains(formatIndex + 1),
arguments[formatIndex + 1].lowercased() == "oci"
{
throw BuilderErrors.invalidArgument(
"'--archive-format oci' cannot be used with '--cross-compile swift-static-sdk'; "
+ "building an OCI image requires a container CLI. Use '--cross-compile docker' or 'container'."
)
}
case "container": crossCompileToolName = "container"
default: crossCompileToolName = "docker"
}
let crossCompileToolPath = try context.tool(named: crossCompileToolName).url
let zipToolPath = try context.tool(named: "zip").url

// Resolve the output directory. The default lives under the plugin's work directory, whose
Expand Down Expand Up @@ -90,7 +123,7 @@ struct AWSLambdaBuilder: CommandPlugin {
"--package-display-name", context.package.displayName,
"--package-directory", context.package.directoryURL.path(),
"--configuration", configurationArgument.first ?? "release",
"--cross-compile-tool-path", containerToolPath.path,
"--cross-compile-tool-path", crossCompileToolPath.path,
"--zip-tool-path", zipToolPath.path,
]
// Re-inject the cross-compilation method (normalised to --cross-compile) so the helper can
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ enum BuildArchitecture: String, Codable, CustomStringConvertible {
#endif
}

/// The Static Linux SDK (musl) target triple for this architecture, passed to
/// `swift build --swift-sdk`.
var muslTriple: String {
switch self {
case .x64: return "x86_64-swift-linux-musl"
case .arm64: return "aarch64-swift-linux-musl"
}
}

/// Parses the `--architecture` value, defaulting to the host architecture when omitted.
static func parse(_ value: String?) throws -> Self {
guard let value else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// Cross-compiles products with the Static Linux SDK (musl), producing a statically-linked binary
/// that runs on Amazon Linux without a container runtime.
///
/// Unlike ``ContainerBuildBackend`` this shells out to `swift` directly on the host — no docker or
/// Apple `container` involved. The target architecture is selected with `--swift-sdk <musl-triple>`,
/// so this backend genuinely cross-compiles (e.g. building an arm64 binary on an x64 host and vice
/// versa), independent of the host architecture.
///
/// The SDK must be installed beforehand (`swift sdk install …`). The plugin's network sandbox
/// (`.docker` scope) forbids downloading it at build time, so this backend detects a missing SDK
/// and fails with install guidance rather than attempting to fetch it.
@available(LambdaSwift 2.0, *)
struct StaticLinuxSDKBuildBackend: BuildBackend {
/// The target CPU architecture, mapped to a musl target triple via ``BuildArchitecture/muslTriple``.
let architecture: BuildArchitecture

/// Path to the `swift` executable resolved by the plugin (the toolchain location differs across
/// hosts, so we never hardcode `/usr/bin/swift` here).
let swiftToolPath: URL

let name = "swift-static-sdk"

func build(
packageIdentity: String,
packageDirectory: URL,
products: [String],
buildConfiguration: BuildConfiguration,
noStrip: Bool,
verboseLogging: Bool
) throws -> [String: URL] {

// verify the swift binary exists at the resolved path
guard FileManager.default.fileExists(atPath: self.swiftToolPath.path()) else {
throw BuilderErrors.swiftToolNotFound(self.swiftToolPath.path())
}

let triple = self.architecture.muslTriple

// Build into a dedicated scratch path, NOT the package's default `.build`. This plugin runs
// as a SwiftPM command plugin, which holds the workspace lock on `.build` for its whole
// duration; a nested `swift build` targeting the same `.build` would block forever waiting
// for that lock. A separate scratch path sidesteps the deadlock (the container backend does
// not hit this because its build runs inside the container, not against the host `.build`).
let scratchPath = packageDirectory.appending(path: ".build").appending(path: "lambda-static-sdk")

// Resolve the build output path with the same `--swift-sdk` selector the build uses. This
// doubles as the SDK preflight: SwiftPM resolves the SDK exactly as a real build would, so
// if no SDK targets the triple this fails, and we surface actionable install guidance. We
// cannot download the SDK ourselves (the plugin sandbox limits network to Docker).
//
// `swift sdk list` is deliberately NOT used here: it prints SDK bundle identifiers (e.g.
// `swift-…_static-linux-0.1.0`), which do not contain the target triple, so matching on the
// triple gives false negatives even when the SDK is installed.
let binPath: String
do {
binPath = try Utils.execute(
executable: self.swiftToolPath,
arguments: [
"build", "-c", buildConfiguration.rawValue,
"--swift-sdk", triple,
"--scratch-path", scratchPath.path(),
"--show-bin-path",
],
customWorkingDirectory: packageDirectory,
logLevel: verboseLogging ? .debug : .silent
).trimmingCharacters(in: .whitespacesAndNewlines)
} catch {
throw BuilderErrors.staticSDKNotInstalled(triple)
}
let buildOutputPath = URL(fileURLWithPath: binPath)

print("-------------------------------------------------------------------------")
print("building \"\(packageIdentity)\" with the Static Linux SDK (\(triple))")
print("-------------------------------------------------------------------------")

var builtProducts = [String: URL]()
for product in products {
print("building \"\(product)\"")
var buildArguments = [
"build", "-c", buildConfiguration.rawValue,
"--product", product,
"--swift-sdk", triple,
"--scratch-path", scratchPath.path(),
"--static-swift-stdlib",
]
if !noStrip {
buildArguments += ["-Xlinker", "-s"]
}
try Utils.execute(
executable: self.swiftToolPath,
arguments: buildArguments,
customWorkingDirectory: packageDirectory,
logLevel: verboseLogging ? .debug : .output
)

let productPath = buildOutputPath.appending(path: product)
guard FileManager.default.fileExists(atPath: productPath.path()) else {
print("expected '\(product)' binary at \"\(productPath.path())\"")
throw BuilderErrors.productExecutableNotFound(product)
}
builtProducts[product] = productPath
}
return builtProducts
}
}
52 changes: 40 additions & 12 deletions Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ struct Builder {
}

// Select the build backend: build natively when already on an Amazon Linux host,
// otherwise cross-compile using the backend chosen by --cross-compile.
// otherwise cross-compile using the backend chosen by --cross-compile. An explicit
// --cross-compile swift-static-sdk always cross-compiles (it can target either
// architecture), so it takes precedence over the native path even on Amazon Linux.
let backend: any BuildBackend
if self.isAmazonLinux(.al2) || self.isAmazonLinux(.al2023) {
if configuration.crossCompileMethod == .swiftStaticSdk {
backend = try configuration.makeCrossCompileBackend()
} else if self.isAmazonLinux(.al2) || self.isAmazonLinux(.al2023) {
// A native build compiles for the host architecture only; it cannot target another one.
// Recording a mismatched architecture in the manifest would recreate the very bug the
// --architecture flag exists to prevent, so reject an explicit cross-architecture request.
Expand Down Expand Up @@ -152,7 +156,10 @@ struct Builder {
--cross-compile <method> The cross-compilation method to use.
Values: docker, container, swift-static-sdk, custom-sdk
(default is docker)
Note: swift-static-sdk and custom-sdk are not yet supported.
swift-static-sdk requires a pre-installed Static Linux
SDK (musl); it needs no docker/container. Install it with
'swift sdk install <url>'.
Note: custom-sdk is not yet supported.
--archive-format <format> The packaging format for the build artifact.
Values: zip, oci
(default is zip)
Expand Down Expand Up @@ -329,15 +336,26 @@ struct BuilderConfiguration: CustomStringConvertible {
/// everything a backend needs (the resolved tool path, base image, and image-update
/// preference), so the factory lives here rather than on ``CrossCompileMethod``.
func makeCrossCompileBackend() throws -> any BuildBackend {
let cli = try self.makeContainerCLI()
return ContainerBuildBackend(
cli: cli,
toolPath: self.crossCompileToolPath,
baseImage: self.baseDockerImage,
disableImageUpdate: self.disableDockerImageUpdate,
architecture: self.architecture,
method: self.crossCompileMethod
)
switch self.crossCompileMethod {
case .docker, .container:
return ContainerBuildBackend(
cli: try self.makeContainerCLI(),
toolPath: self.crossCompileToolPath,
baseImage: self.baseDockerImage,
disableImageUpdate: self.disableDockerImageUpdate,
architecture: self.architecture,
method: self.crossCompileMethod
)
case .swiftStaticSdk:
// The Static Linux SDK build shells out to `swift`, not a container CLI. The plugin
// resolves the swift toolchain and forwards its path via --cross-compile-tool-path.
return StaticLinuxSDKBuildBackend(
architecture: self.architecture,
swiftToolPath: self.crossCompileToolPath
)
case .customSdk:
throw BuilderErrors.unsupportedCrossCompileMethod(self.crossCompileMethod)
}
}

/// Resolves the ``ContainerCLI`` argument flavor for the configured cross-compile method.
Expand Down Expand Up @@ -402,6 +420,8 @@ enum BuilderErrors: Error, CustomStringConvertible {
case unsupportedCrossCompileMethod(CrossCompileMethod)
case unsupportedArchiveFormat(ArchiveFormat)
case containerCLINotFound(CrossCompileMethod)
case swiftToolNotFound(String)
case staticSDKNotInstalled(String)
case failedWritingDockerfile
case failedParsingDockerOutput(String)
case processFailed([String], Int32)
Expand Down Expand Up @@ -439,6 +459,14 @@ enum BuilderErrors: Error, CustomStringConvertible {
+ "For information on how to install and use Swift cross-compilation SDKs, visit: "
+ "https://www.swift.org/documentation/articles/static-linux-getting-started.html"
}
case .swiftToolNotFound(let path):
return "The 'swift' executable was not found at the expected path '\(path)'."
case .staticSDKNotInstalled(let triple):
return
"No Static Linux SDK targeting '\(triple)' is installed. "
+ "Install it with 'swift sdk install <url>' and try again. "
+ "For information on how to install and use Swift cross-compilation SDKs, visit: "
+ "https://www.swift.org/documentation/articles/static-linux-getting-started.html"
case .failedWritingDockerfile:
return "failed writing dockerfile"
case .failedParsingDockerOutput(let output):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ enum CrossCompileMethod: String, CustomStringConvertible {

var isSupported: Bool {
switch self {
case .docker, .container: return true
case .swiftStaticSdk, .customSdk: return false
case .docker, .container, .swiftStaticSdk: return true
case .customSdk: return false
}
}

Expand Down
Loading