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
92 changes: 42 additions & 50 deletions Plugins/AWSLambdaBuilder/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,86 +21,78 @@ struct AWSLambdaBuilder: CommandPlugin {

func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws {

// values to pass to the AWSLambdaPluginHelper
let outputDirectory: URL
let products: [Product]
let buildConfiguration: PackageManager.BuildConfiguration
let packageID: String = context.package.id
let packageDisplayName = context.package.displayName
let packageDirectory = context.package.directoryURL
let zipToolPath = try context.tool(named: "zip").url

// extract arguments that require PluginContext to fully resolve
// resolve them here and pass them to the AWSLambdaPluginHelper as arguments
// This plugin is a thin layer over the AWSLambdaPluginHelper executable. It resolves only
// the values that require the PackagePlugin sandbox or the package graph, injects them as
// canonical arguments, then forwards every argument it did not consume to the helper. The
// helper owns the remaining argument parsing and defaulting.
var argumentExtractor = ArgumentExtractor(arguments)

// Options the plugin resolves itself. These are consumed (extracted) here so they are not
// also forwarded via remainingArguments, which would pass them to the helper twice.
let outputPathArgument = argumentExtractor.extractOption(named: "output-path")
let productsArgument = argumentExtractor.extractOption(named: "products")
// 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")

// 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. Extracting
// these options only peeks them for the plugin; the original `arguments` (which the helper
// re-parses) is still forwarded unchanged below.
// `--container-cli` is a deprecated alias for `--cross-compile`.
let crossCompileArgument = argumentExtractor.extractOption(named: "cross-compile")
let containerCliArgument = argumentExtractor.extractOption(named: "container-cli")

// 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 containerCLIToolName = crossCompileMethod == "container" ? "container" : "docker"
let containerToolPath = try context.tool(named: containerCLIToolName).url
let zipToolPath = try context.tool(named: "zip").url

// Resolve the output directory. The default lives under the plugin's work directory, whose
// location is only known to the plugin. This path is part of the plugin's public contract
// (documented and consumed by lambda-deploy), so it must stay stable.
let outputDirectory: URL
if let outputPath = outputPathArgument.first {
#if os(Linux)
var isDirectory: Bool = false
#else
var isDirectory: ObjCBool = false
#endif
guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory)
else {
guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory) else {
throw BuilderErrors.invalidArgument("invalid output directory '\(outputPath)'")
}
outputDirectory = URL(fileURLWithPath: outputPath)
} else {
outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaBuilder.self)")
}

let explicitProducts = !productsArgument.isEmpty
if explicitProducts {
let _products = try context.package.products(named: productsArgument)
for product in _products {
guard product is ExecutableProduct else {
throw BuilderErrors.invalidArgument("product named '\(product.name)' is not an executable product")
}
}
products = _products

} else {
// Resolve and validate the products against the package graph.
let products: [Product]
if productsArgument.isEmpty {
products = context.package.products.filter { $0 is ExecutableProduct }
}

if let _buildConfigurationName = configurationArgument.first {
guard let _buildConfiguration = PackageManager.BuildConfiguration(rawValue: _buildConfigurationName) else {
throw BuilderErrors.invalidArgument("invalid build configuration named '\(_buildConfigurationName)'")
}
buildConfiguration = _buildConfiguration
} else {
buildConfiguration = .release
products = try context.package.products(named: productsArgument)
for product in products where !(product is ExecutableProduct) {
throw BuilderErrors.invalidArgument("product named '\(product.name)' is not an executable product")
}
}

let tool = try context.tool(named: "AWSLambdaPluginHelper")
let args =
[
"build",
"--output-path", outputDirectory.path(),
"--products", products.map { $0.name }.joined(separator: ","),
"--configuration", buildConfiguration.rawValue,
"--package-id", packageID,
"--package-display-name", packageDisplayName,
"--package-directory", packageDirectory.path(),
"--docker-tool-path", containerToolPath.path,
"--zip-tool-path", zipToolPath.path,
] + arguments
var args = [
"build",
"--output-path", outputDirectory.path(),
"--products", products.map { $0.name }.joined(separator: ","),
"--package-id", context.package.id,
"--package-display-name", context.package.displayName,
"--package-directory", context.package.directoryURL.path(),
"--configuration", configurationArgument.first ?? "release",
"--cross-compile-tool-path", containerToolPath.path,
"--zip-tool-path", zipToolPath.path,
]
// Re-inject the cross-compilation method (normalised to --cross-compile) so the helper can
// select the build method, then forward everything the plugin did not consume.
if let crossCompileMethod {
args += ["--cross-compile", crossCompileMethod]
}
args += argumentExtractor.remainingArguments

// Invoke the plugin helper, passing the current environment so that
// AWS credentials and HOME are available to the subprocess.
Expand Down
6 changes: 4 additions & 2 deletions Plugins/AWSLambdaDeployer/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ struct AWSLambdaDeployer: CommandPlugin {

let tool = try context.tool(named: "AWSLambdaPluginHelper")

// Resolve products: use --products if provided, otherwise default to all executable targets
// Resolve products: use --products if provided, otherwise default to all executable targets.
// --products is consumed (extracted) here so it is not also forwarded via remainingArguments,
// which would pass it to the helper twice.
var argumentExtractor = ArgumentExtractor(arguments)
let productsArgument = argumentExtractor.extractOption(named: "products")

Expand All @@ -36,7 +38,7 @@ struct AWSLambdaDeployer: CommandPlugin {

let productNames = products.map { $0.name }.joined(separator: ",")

let args = ["deploy", "--products", productNames] + arguments
let args = ["deploy", "--products", productNames] + argumentExtractor.remainingArguments

// Invoke the plugin helper, passing the current environment so that
// AWS credentials (env vars, HOME for ~/.aws/credentials) are available.
Expand Down
92 changes: 44 additions & 48 deletions Plugins/AWSLambdaPackager/Plugin@swift-6.4.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,34 +32,38 @@ struct AWSLambdaPackager: CommandPlugin {
"'archive' is deprecated. Please use 'swift package lambda-build' instead."
)

// Resolve context-dependent values (same as AWSLambdaBuilder)
let outputDirectory: URL
let products: [Product]
let buildConfiguration: PackageManager.BuildConfiguration
let packageID: String = context.package.id
let packageDisplayName = context.package.displayName
let packageDirectory = context.package.directoryURL
let zipToolPath = try context.tool(named: "zip").url

// This deprecated command is a thin layer over the AWSLambdaPluginHelper executable. It
// resolves only the values that require the PackagePlugin sandbox or the package graph,
// injects them as canonical arguments, then forwards every argument it did not consume to
// the helper. The helper owns the remaining argument parsing and defaulting.
// It mirrors AWSLambdaBuilder, delegating to the same `build` helper subcommand.
var argumentExtractor = ArgumentExtractor(arguments)

// Options the plugin resolves itself. These are consumed (extracted) here so they are not
// also forwarded via remainingArguments, which would pass them to the helper twice.
let outputPathArgument = argumentExtractor.extractOption(named: "output-path")
// `--output-directory` is a deprecated alias for `--output-path`.
let outputDirectoryArgument = argumentExtractor.extractOption(named: "output-directory")
let productsArgument = argumentExtractor.extractOption(named: "products")
// 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")

// 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 `container`, `docker` otherwise. Extracting these options
// only peeks them for the plugin; the original `arguments` (which the helper re-parses) is
// still forwarded unchanged below. `--container-cli` is the legacy alias for `--cross-compile`.
// `--container-cli` is a deprecated alias for `--cross-compile`.
let crossCompileArgument = argumentExtractor.extractOption(named: "cross-compile")
let containerCliArgument = argumentExtractor.extractOption(named: "container-cli")

// 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 containerCLIToolName = crossCompileMethod == "container" ? "container" : "docker"
let containerToolPath = try context.tool(named: containerCLIToolName).url
let zipToolPath = try context.tool(named: "zip").url

// output directory
// Resolve the output directory. The default lives under the plugin's work directory, whose
// location is only known to the plugin. This path is part of the plugin's public contract
// (documented and consumed by lambda-deploy), so it must stay stable.
let outputDirectory: URL
if let outputPath = outputPathArgument.first ?? outputDirectoryArgument.first {
#if os(Linux)
var isDirectory: Bool = false
Expand All @@ -75,44 +79,36 @@ struct AWSLambdaPackager: CommandPlugin {
outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)")
}

// products
let explicitProducts = !productsArgument.isEmpty
if explicitProducts {
let _products = try context.package.products(named: productsArgument)
for product in _products {
guard product is ExecutableProduct else {
throw PackagerErrors.invalidArgument("product named '\(product.name)' is not an executable product")
}
}
products = _products
} else {
// Resolve and validate the products against the package graph.
let products: [Product]
if productsArgument.isEmpty {
products = context.package.products.filter { $0 is ExecutableProduct }
}

// build configuration
if let buildConfigurationName = configurationArgument.first {
guard let _buildConfiguration = PackageManager.BuildConfiguration(rawValue: buildConfigurationName) else {
throw PackagerErrors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'")
}
buildConfiguration = _buildConfiguration
} else {
buildConfiguration = .release
products = try context.package.products(named: productsArgument)
for product in products where !(product is ExecutableProduct) {
throw PackagerErrors.invalidArgument("product named '\(product.name)' is not an executable product")
}
}

// Build the resolved arguments for the helper
let tool = try context.tool(named: "AWSLambdaPluginHelper")
let args =
[
"build",
"--output-path", outputDirectory.path(),
"--products", products.map { $0.name }.joined(separator: ","),
"--configuration", buildConfiguration.rawValue,
"--package-id", packageID,
"--package-display-name", packageDisplayName,
"--package-directory", packageDirectory.path(),
"--docker-tool-path", containerToolPath.path,
"--zip-tool-path", zipToolPath.path,
] + arguments
var args = [
"build",
"--output-path", outputDirectory.path(),
"--products", products.map { $0.name }.joined(separator: ","),
"--package-id", context.package.id,
"--package-display-name", context.package.displayName,
"--package-directory", context.package.directoryURL.path(),
"--configuration", configurationArgument.first ?? "release",
"--cross-compile-tool-path", containerToolPath.path,
"--zip-tool-path", zipToolPath.path,
]
// Re-inject the cross-compilation method (normalised to --cross-compile) so the helper can
// select the build method, then forward everything the plugin did not consume.
if let crossCompileMethod {
args += ["--cross-compile", crossCompileMethod]
}
args += argumentExtractor.remainingArguments

// Invoke the plugin helper, passing the current environment
let process = Process()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// The Apple `container` CLI argument flavor (https://github.com/apple/container).
///
/// This type builds its complete argument vector on its own and shares no helper with other
/// ``ContainerCLI`` implementations — see the note on ``ContainerCLI``. Its argument layout
/// deliberately differs from Docker's where the CLIs differ: the image subcommand is
/// `image pull` rather than `pull`, and the runtime needs an explicit `--memory` reservation.
@available(LambdaSwift 2.0, *)
struct AppleContainerCLI: ContainerCLI {
let executableName = "container"

func pullArguments(image: String) -> [String] {
["image", "pull", image]
}

func runArguments(
baseImage: String,
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"]
for mount in mounts {
args += ["-v", mount]
}
if let env {
for (key, value) in env.sorted(by: { $0.key < $1.key }) {
args += ["-e", "\(key)=\(value)"]
}
}
args += ["-w", workingDirectory, baseImage, "bash", "-cl", command]
return args
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//===----------------------------------------------------------------------===//
//
// 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

/// A strategy for building Lambda product executables for the Amazon Linux target.
///
/// A backend owns *how* a build is performed — natively on an Amazon Linux host, inside a
/// container (docker, Apple's `container`, …), or via a Swift cross-compilation SDK. Backend
/// selection lives in ``Builder`` and ``CrossCompileMethod/makeBackend(configuration:)``; the
/// configuration each backend needs (tool paths, base image, …) is injected through its
/// initializer rather than this protocol, so the protocol stays stable as new backends are added.
@available(LambdaSwift 2.0, *)
protocol BuildBackend {
/// A human-readable name used in log output (e.g. "docker", "container", "native").
var name: String { get }

/// Build the requested products and return a map of product name to the built binary's
/// location on the host filesystem.
///
/// - Parameters:
/// - packageIdentity: The package identity, used for log output.
/// - packageDirectory: The root directory of the package being built.
/// - products: The executable product names to build.
/// - buildConfiguration: `debug` or `release`.
/// - noStrip: When `true`, debug symbols are not stripped from the binary.
/// - verboseLogging: When `true`, emit verbose output for debugging.
/// - Returns: A map of product name to the built executable's URL on the host.
func build(
packageIdentity: String,
packageDirectory: URL,
products: [String],
buildConfiguration: BuildConfiguration,
noStrip: Bool,
verboseLogging: Bool
) throws -> [String: URL]
}
Loading