A pure-Swift container build system #401
Replies: 2 comments
-
|
Thanks for kicking this off, @katiewasnothere ! For everyone, the design docs bundled in PR #399 outline our goals, the proposed build-graph model, and the initial implementation. We’d love your eyes on those details. Especially around layer caching, multi-arch strategy, and dependency resolution. If there are capabilities you’d like us to consider early (e.g., registry push/pull hooks, progress UI, SBOM generation), please drop them here. The sooner we know your priorities, the better we can shape the roadmap. Looking forward to your feedback and to building this together to make container builds in Swift a first-class experience! 🚀 |
Beta Was this translation helpful? Give feedback.
-
|
Hi everyone, we've been steadily progressing with the pure-swift builder implementation. The IR and build graph were introduced in the first commit. Then, we achieved a good amount of coverage of Dockerfile instructions in the parser. The differ/snapshotter impl in progress. Here is a detailed design/implementation proposal for implementing Filesystem Executor. We are following a unique approach for snapshotting image layers into a single EXT4 block device. This has been made possible, thanks to our earlier work on the pure-swift EXT4 formatter. TL;DR: The filesystem executor talks directly to pure‑Swift EXT4 stack from High level design
Containerization EXT4’s design exposes the container filesystem as a block device and ships a Swift EXT4 library to format and populate that device, which is exactly what our executor needs. Mapping Containerization’s EXT4 APIs to FS ExecutorContainerization EXT4 module
Executor architecture
APIThe following protocol is defined in such a way that it can be mocked for tests: // A thin wrapper over ContainerizationEXT4
public protocol EXT4WritableFS: Sendable {
func create(
path: String,
link: String?, // symlink or hardlink target (see semantics below)
mode: UInt16, // POSIX mode (0o755, etc.)
ts: (atime: UInt64, mtime: UInt64, ctime: UInt64)?,
buf: UnsafeRawBufferPointer?, // nil for non-regular files
uid: UInt32,
gid: UInt32,
xattrs: [String: Data]?,
recursion: Bool
) throws
func unpack(
sourceFD: Int32, // tar/zip stream (seekable not required)
format: ArchiveFormat, // .tar, .zip, ...
compression: Compression // .none, .gzip, .bzip2, .xz
) throws
}
public enum ArchiveFormat: Sendable { case tar, zip } // map 1:1 to EXT4.Formatter enums
public enum Compression: Sendable { case none, gzip, bzip2, xz }
AdapterThe adapter bridges EXT4Formatter and EXT4WritableFS: import ContainerizationEXT4
import Foundation
public final class Ext4FormatterAdapter: Ext4WritableFS {
private let formatter: EXT4.Formatter
// obtain `formatter` from snapshotter open/prepare step.
public init(formatter: EXT4.Formatter) { self.formatter = formatter }
public func create(
path: String,
link: String?,
mode: UInt16,
ts: (atime: UInt64, mtime: UInt64, ctime: UInt64)?,
buf: UnsafeRawBufferPointer?,
uid: UInt32,
gid: UInt32,
xattrs: [String : Data]?,
recursion: Bool
) throws {
// Direct pass-through to ContainerizationEXT4
try formatter.create(
path: path,
link: link,
mode: mode,
ts: ts,
buf: buf,
uid: uid,
gid: gid,
xattrs: xattrs,
recursion: recursion
)
}
public func unpack(sourceFD: Int32, format: ArchiveFormat, compression: Compression) throws {
// Map our enums to the ContainerizationEXT4 enums.
let fmt: EXT4.Formatter.ArchiveFormat = (format == .tar) ? .tar : .zip
let cmp: EXT4.Formatter.Compression
switch compression {
case .none: cmp = .none
case .gzip: cmp = .gzip
case .bzip2: cmp = .bzip2
case .xz: cmp = .xz
}
try formatter.unpack(source: .fileDescriptor(sourceFD), format: fmt, compression: cmp, progress: nil)
}
}Wiring into
|
| IR action | EXT4.Formatter call | Notes |
|---|---|---|
copy file |
create(path:..., buf: <bytes>) |
Set mode/uid/gid/xattrs/ts |
copy dir |
create(path:..., buf: nil, recursion: true) |
Pre‑create dir before children |
add tar(.gz/.bz2/.xz) |
unpack(source:fd, format:.tar, compression:...) |
Docker ADD behavior |
add regular file |
same as copy |
No auto‑extract |
mkdir |
create(path:..., buf: nil, recursion: true) |
Respect Permissions |
symlink |
create(path:..., link:"target", buf:nil) |
mode isn’t meaningfully applied |
hardlink |
create(path:..., link:"/existing", buf:nil) |
Error if link target is a dir (see Error enum) |
remove |
(EXT4 delete) | Use EXT4 delete primitive; not shown here |
Security model
- Path traversal & canonicalization — Always run
normalizedContainerPath. - Symlink escape — Resolve any symlinks in the source before copying; treat link targets as opaque bytes in the destination (don’t resolve to host).
- Archive safety — For
unpack, deny absolute and..entries; rely onEXT4.Formatter.unpack’s validation, and still double‑normalize per entry to defense‑in‑depth. - Permissions — Validate octal modes, deny setuid/setgid unless explicitly allowed by policy.
- Xattrs — Bound total xattr bytes per file and per layer; apply only when
preserveXattrsis set.
Concurrency & performance
- Use a
TaskGroupwith a global cap (10) to parallelize independent file materializations, but serialize calls that must be ordered (e.g., creating parent dirs before children). - Stream large inputs (
ADDarchives or >64MiB files) straight intocreate/unpack(no whole‑blob buffering). - Cache keys must include: base snapshot ID, source digests (content hash), dest paths, metadata flags, and follow‑symlink policy.
Errors & diagnostics
Translate EXT4 errors to structured error model:
EXT4.Formatter.Error.alreadyExists→ conflict underfailstrategyEXT4.Formatter.Error.cannotCreateHardlinksToDirTarget→ invalid hardlink target
Propagate the IR’sOperationMetadata(Dockerfile location) onto every thrown error for precise messages.
Snapshotter & Differ glue
The snapshotter already exposes prepare/commit and a Differ. Just ensure the writable snapshot exposes an fd (or path) to the backing block file so we can create an EXT4.Formatter on it and hand an EXT4WritableFS to the executor. (Containerization’s repos and docs describe the block‑device model and module split into ContainerizationOCI, ContainerizationEXT4, ContainerizationArchive.)
Docker compatibility edge‑cases
ADDof*.tar,*.tar.gz,*.tgz,*.tar.xz,*.zip→ auto‑extract with path filteringCOPYof archives → no auto‑extract--chownUID/GID by name vs numeric (resolve with /etc/passwd from build stage if present)--chmodoctal parse & recursive application- Symlink loops, absolute symlinks (
/usr/bin/python) remain links, not resolved to host - Hardlinks that target directories → error (propagate
Formatter.Error)
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Many of the container maintainers and I have been working on creating a native container builder to eventually replace our https://github.com/apple/container-builder-shim use. I've opened a PR here #399 with our initial design and partial implementations. There are some docs included in that PR that describe our approach and some of our goals.
We'd very much like to make this a community effort and we're open to all feedback on the design and initial work!
@wlan0
Beta Was this translation helpful? Give feedback.
All reactions