Files
Kathryn Baldauf 16f2630126 Add initial native builder code (#399)
We're working on making a pure swift container image build system that
leverages containerization. This PR represents our initial design and
initial work towards this goal.

The native builder is still in active development and most of the
implementation has not been started or completed. We will be opening a
series of issues that represent various (but not necessarily all) pieces
of work that need to be done here.

There are docs included in this PR that describe the overall design of
each component and outline some of our goals. The easiest way to view
the docs by themselves (since this is a massive PR) is to look at the
docs commit in the `Commits` tab.

We'd love any feedback! 

@wlan0

---------

Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
2025-07-31 13:13:20 -07:00

289 lines
9.5 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationOCI
import Crypto
import Foundation
/// Errors that can occur during operation processing.
public enum OperationError: LocalizedError {
case encodingFailed(String)
case digestComputationFailed(Error)
public var errorDescription: String? {
switch self {
case .encodingFailed(let details):
return "Failed to encode operation data: \(details)"
case .digestComputationFailed(let error):
return "Failed to compute operation digest: \(error.localizedDescription)"
}
}
}
/// Core operation protocol that all build operations must conform to.
///
/// Design rationale:
/// - Protocol-based design allows extending with new operations without modifying existing code
/// - Each operation is self-contained with all necessary data
/// - Operations are immutable for thread safety and predictable behavior
/// - Visitor pattern support for traversal and transformation
public protocol Operation: Sendable {
/// Common metadata associated with this operation.
var metadata: OperationMetadata { get }
/// Unique identifier for this operation type
static var operationKind: OperationKind { get }
/// Instance identifier
var operationKind: OperationKind { get }
/// Accept a visitor for traversal/transformation
func accept<V: OperationVisitor>(_ visitor: V) throws -> V.Result
}
/// Identifies the type of operation.
///
/// Design rationale:
/// - String-based for extensibility (third-party operations)
/// - Comparable for consistent ordering
/// - Provides namespace for operation types
public struct OperationKind: RawRepresentable, Hashable, Sendable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
// Core operation kinds
public static let exec = OperationKind(rawValue: "core.exec")
public static let filesystem = OperationKind(rawValue: "core.filesystem")
public static let image = OperationKind(rawValue: "core.image")
public static let metadata = OperationKind(rawValue: "core.metadata")
public static let mount = OperationKind(rawValue: "core.mount")
}
/// Visitor pattern for operation traversal.
///
/// Design rationale:
/// - Type-safe traversal without casting
/// - Extensible for new operations
/// - Supports both read-only traversal and transformation
public protocol OperationVisitor {
associatedtype Result
func visit(_ operation: ExecOperation) throws -> Result
func visit(_ operation: FilesystemOperation) throws -> Result
func visit(_ operation: ImageOperation) throws -> Result
func visit(_ operation: MetadataOperation) throws -> Result
/// Default handler for unknown operations
func visitUnknown(_ operation: any Operation) throws -> Result
}
/// Base class for operations providing common functionality.
///
/// Design rationale:
/// - While we prefer protocols, a base class here provides default implementations
/// - Reduces boilerplate for operation implementations
/// - Still allows protocol-based extension
@available(*, unavailable, message: "Use specific operation types instead")
open class BaseOperation: @unchecked Sendable, Operation {
public var metadata: OperationMetadata
public init(metadata: OperationMetadata) {
self.metadata = metadata
}
public static var operationKind: OperationKind {
// This class is unavailable for use, but we need to provide a value
// for protocol conformance. This should never be called in practice.
OperationKind(rawValue: "base.unavailable")
}
public var operationKind: OperationKind { Self.operationKind }
public func accept<V: OperationVisitor>(_ visitor: V) throws -> V.Result {
try visitor.visitUnknown(self)
}
}
// MARK: - Operation Metadata
/// Common metadata that can be attached to any operation.
///
/// Design rationale:
/// - Extensible key-value storage for future needs
/// - Strongly-typed for common attributes
/// - Preserves unknown attributes for forward compatibility
public struct OperationMetadata: Sendable, Hashable {
/// Human-readable comment/description
public let comment: String?
/// Source location (file:line) where this operation was defined
public let sourceLocation: SourceLocation?
/// Platform constraints for this operation
public let platforms: Set<Platform>?
/// Cache configuration
public let cacheConfig: CacheConfig?
/// The retry policy for this specific operation.
public let retryPolicy: RetryPolicy
/// Additional attributes
public let attributes: [String: AttributeValue]
public init(
comment: String? = nil,
sourceLocation: SourceLocation? = nil,
platforms: Set<Platform>? = nil,
cacheConfig: CacheConfig? = nil,
retryPolicy: RetryPolicy = RetryPolicy(maxRetries: 0),
attributes: [String: AttributeValue] = [:]
) {
self.comment = comment
self.sourceLocation = sourceLocation
self.platforms = platforms
self.cacheConfig = cacheConfig
self.retryPolicy = retryPolicy
self.attributes = attributes
}
}
/// Defines the retry behavior for a failed operation.
public struct RetryPolicy: Sendable, Hashable, Codable {
/// The maximum number of times to retry the operation. A value of 0 means no retries.
public let maxRetries: Int
/// The multiplier to apply to the delay between retries. A value of 1.0 is a linear backoff.
public let backoffMultiplier: Double
/// The initial delay to wait before the first retry.
public let initialDelay: TimeInterval
/// The maximum possible delay between retries.
public let maxDelay: TimeInterval
public init(
maxRetries: Int = 3,
backoffMultiplier: Double = 2.0,
initialDelay: TimeInterval = 1.0,
maxDelay: TimeInterval = 60.0
) {
self.maxRetries = maxRetries
self.backoffMultiplier = backoffMultiplier
self.initialDelay = initialDelay
self.maxDelay = maxDelay
}
}
// MARK: - Operation Extensions
extension Operation {
/// Compute a content digest for cache key generation using stable serialization
/// - Throws: OperationError if encoding or digest computation fails
public func contentDigest() throws -> Digest {
var hasher = SHA256()
// For now, use a simple string representation of the operation
// In production, this would need proper type-specific hashing
let operationString = String(describing: self)
guard let operationData = operationString.data(using: String.Encoding.utf8) else {
// This should never happen as String descriptions are valid UTF-8
throw OperationError.encodingFailed("Failed to encode operation string as UTF-8: \(operationString)")
}
hasher.update(data: operationData)
let digest = hasher.finalize()
do {
return try Digest(algorithm: .sha256, bytes: Data(digest))
} catch {
// This should never happen as SHA256 produces correct byte length
throw OperationError.digestComputationFailed(error)
}
}
}
/// Source location information.
public struct SourceLocation: Sendable, Hashable {
public let file: String
public let line: Int
public let column: Int?
public init(file: String, line: Int, column: Int? = nil) {
self.file = file
self.line = line
self.column = column
}
}
/// Cache configuration for operations.
public struct CacheConfig: Sendable, Hashable {
public enum CacheMode: String, Sendable, Hashable {
case `default`
case none
case locked
case shared
}
public let mode: CacheMode
public let id: String?
public let sharing: SharingMode?
public init(mode: CacheMode = .default, id: String? = nil, sharing: SharingMode? = nil) {
self.mode = mode
self.id = id
self.sharing = sharing
}
}
/// Cache sharing mode.
public enum SharingMode: String, Sendable, Hashable {
case locked
case shared
case `private`
}
/// Attribute value for extensible metadata.
public enum AttributeValue: Sendable, Hashable {
case string(String)
case integer(Int)
case double(Double)
case boolean(Bool)
case data(Data)
case array([AttributeValue])
case dictionary([String: AttributeValue])
}
// MARK: - Codable Support
extension OperationKind: Codable {}
extension OperationMetadata: Codable {
// Implementation would handle encoding/decoding of all fields
}
extension SourceLocation: Codable {}
extension CacheConfig.CacheMode: Codable {}
extension CacheConfig: Codable {}
extension SharingMode: Codable {}
extension AttributeValue: Codable {
// Implementation would handle all cases
}