Skip to content
Open
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
4 changes: 3 additions & 1 deletion Plugins/ContainerImageBuilder/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ extension PluginError: CustomStringConvertible {
resources.map { "--resources=\($0)" }
+ builtExecutables.map { $0.url.path }
+ extractor.remainingArguments
let helperEnv = ProcessInfo.processInfo.environment.filter { $0.key.starts(with: "CONTAINERTOOL_") }
let helperEnv = ProcessInfo.processInfo.environment.filter {
$0.key.starts(with: "CONTAINERTOOL_") || $0.key == "SOURCE_DATE_EPOCH"
}

let err = Pipe()

Expand Down
16 changes: 8 additions & 8 deletions Sources/Tar/tar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,32 +432,32 @@ extension Archive {
/// - name: File name
/// - prefix: Path prefix
/// - data: File contents
public mutating func appendFile(name: String, prefix: String = "", data: [UInt8]) throws {
try append(.init(header: .init(name: name, size: data.count, prefix: prefix), data: data))
public mutating func appendFile(name: String, prefix: String = "", data: [UInt8], mtime: Int = 0) throws {
try append(.init(header: .init(name: name, size: data.count, prefix: prefix, mtime: mtime), data: data))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mtime must come before prefix.

}

/// Adds a new file member at the end of the archive
/// parameters:
/// - name: File name
/// - prefix: Path prefix
/// - data: File contents
public func appendingFile(name: String, prefix: String = "", data: [UInt8]) throws -> Self {
try appending(.init(header: .init(name: name, size: data.count, prefix: prefix), data: data))
public func appendingFile(name: String, prefix: String = "", data: [UInt8], mtime: Int = 0) throws -> Self {
try appending(.init(header: .init(name: name, size: data.count, prefix: prefix, mtime: mtime), data: data))
}

/// Adds a new directory member at the end of the archive
/// parameters:
/// - name: Directory name
/// - prefix: Path prefix
public mutating func appendDirectory(name: String, prefix: String = "") throws {
try append(.init(header: .init(name: name, typeflag: .DIRTYPE, prefix: prefix)))
public mutating func appendDirectory(name: String, prefix: String = "", mtime: Int = 0) throws {
try append(.init(header: .init(name: name, typeflag: .DIRTYPE, prefix: prefix, mtime: mtime)))
}

/// Adds a new directory member at the end of the archive
/// parameters:
/// - name: Directory name
/// - prefix: Path prefix
public func appendingDirectory(name: String, prefix: String = "") throws -> Self {
try self.appending(.init(header: .init(name: name, typeflag: .DIRTYPE, prefix: prefix)))
public func appendingDirectory(name: String, prefix: String = "", mtime: Int = 0) throws -> Self {
try self.appending(.init(header: .init(name: name, typeflag: .DIRTYPE, prefix: prefix, mtime: mtime)))
}
}
123 changes: 63 additions & 60 deletions Sources/containertool/Extensions/Archive+appending.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,80 +20,83 @@ import struct Foundation.URL
import Tar

extension URL {
var isDirectory: Bool {
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
}
var isDirectory: Bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't reindent the code. There is an automated formatting check which won't pass unless the indentation is consistent.

(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
}
}

extension Archive {
/// Append a file or directory tree to the archive. Directory trees are appended recursively.
/// Parameters:
/// - root: The path to the file or directory to add.
/// Returns: A new archive made by appending `root` to the receiver.
public func appendingRecursively(atPath root: String) throws -> Self {
let url = URL(fileURLWithPath: root)
if url.isDirectory {
return try self.appendingDirectoryTree(at: url)
} else {
return try self.appendingFile(at: url)
/// Append a file or directory tree to the archive. Directory trees are appended recursively.
/// Parameters:
/// - root: The path to the file or directory to add.
/// - mtime: Modification time for all entries as seconds since the Unix epoch (default: 0).
/// Returns: A new archive made by appending `root` to the receiver.
public func appendingRecursively(atPath root: String, mtime: Int = 0) throws -> Self {
let url = URL(fileURLWithPath: root)
if url.isDirectory {
return try self.appendingDirectoryTree(at: url, mtime: mtime)
} else {
return try self.appendingFile(at: url, mtime: mtime)
}
}

/// Append a single file to the archive.
/// Parameters:
/// - path: The path to the file to add.
/// - mtime: Modification time as seconds since the Unix epoch (default: 0).
/// Returns: A new archive made by appending `path` to the receiver.
func appendingFile(at path: URL, mtime: Int = 0) throws -> Self {
try self.appendingFile(name: path.lastPathComponent, data: try [UInt8](Data(contentsOf: path)), mtime: mtime)
}
}

/// Append a single file to the archive.
/// Parameters:
/// - path: The path to the file to add.
/// Returns: A new archive made by appending `path` to the receiver.
func appendingFile(at path: URL) throws -> Self {
try self.appendingFile(name: path.lastPathComponent, data: try [UInt8](Data(contentsOf: path)))
}
func appendingFile(at path: URL, to destinationPath: URL, mtime: Int = 0) throws -> Self {
var ret = self
let data = try [UInt8](Data(contentsOf: path))
let components = destinationPath.pathComponents
precondition(!components.isEmpty, "Destination path is empty")
for i in 1..<components.count - 1 {
let directoryPath = components[..<i].joined(separator: "/")
try ret.appendDirectory(name: directoryPath, mtime: mtime)
}

func appendingFile(at path: URL, to destinationPath: URL) throws -> Self {
var ret = self
let data = try [UInt8](Data(contentsOf: path))
let components = destinationPath.pathComponents
precondition(!components.isEmpty, "Destination path is empty")
for i in 1..<components.count - 1 {
let directoryPath = components[..<i].joined(separator: "/")
try ret.appendDirectory(name: directoryPath)
try ret.appendFile(name: destinationPath.path(), data: data, mtime: mtime)
return ret
}

try ret.appendFile(name: destinationPath.path(), data: data)
return ret
}
/// Recursively append a single directory tree to the archive.
/// Parameters:
/// - root: The path to the directory to add.
/// - mtime: Modification time for all entries as seconds since the Unix epoch (default: 0).
/// Returns: A new archive made by appending `root` to the receiver.
func appendingDirectoryTree(at root: URL, to destinationPath: URL = URL(filePath: "/"), mtime: Int = 0) throws -> Self {
var ret = self

/// Recursively append a single directory tree to the archive.
/// Parameters:
/// - root: The path to the directory to add.
/// Returns: A new archive made by appending `root` to the receiver.
func appendingDirectoryTree(at root: URL, to destinationPath: URL = URL(filePath: "/")) throws -> Self {
var ret = self
guard let enumerator = FileManager.default.enumerator(atPath: root.path) else {
throw ("Unable to read \(root.path)")
}

guard let enumerator = FileManager.default.enumerator(atPath: root.path) else {
throw ("Unable to read \(root.path)")
}
for case let subpath as String in enumerator {
// https://developer.apple.com/documentation/foundation/filemanager/1410452-attributesofitem
// https://developer.apple.com/documentation/foundation/fileattributekey

for case let subpath as String in enumerator {
// https://developer.apple.com/documentation/foundation/filemanager/1410452-attributesofitem
// https://developer.apple.com/documentation/foundation/fileattributekey
guard let filetype = enumerator.fileAttributes?[.type] as? FileAttributeType else {
throw ("Unable to get file type for \(subpath)")
}

guard let filetype = enumerator.fileAttributes?[.type] as? FileAttributeType else {
throw ("Unable to get file type for \(subpath)")
}
let subpath = destinationPath.appending(path: subpath).path()
switch filetype {
case .typeRegular:
let resource = try [UInt8](Data(contentsOf: root.appending(path: subpath)))
try ret.appendFile(name: subpath, prefix: root.lastPathComponent, data: resource, mtime: mtime)

let subpath = destinationPath.appending(path: subpath).path()
switch filetype {
case .typeRegular:
let resource = try [UInt8](Data(contentsOf: root.appending(path: subpath)))
try ret.appendFile(name: subpath, prefix: root.lastPathComponent, data: resource)
case .typeDirectory:
try ret.appendDirectory(name: subpath, prefix: root.lastPathComponent, mtime: mtime)

case .typeDirectory:
try ret.appendDirectory(name: subpath, prefix: root.lastPathComponent)
default:
throw "Resource file \(subpath) of type \(filetype) is not supported"
}
}

default:
throw "Resource file \(subpath) of type \(filetype) is not supported"
}
return ret
}

return ret
}
}
18 changes: 14 additions & 4 deletions Sources/containertool/Extensions/RegistryClient+publish.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import class Foundation.FileManager
import struct Foundation.ObjCBool
import class Foundation.ProcessInfo
import struct Foundation.Date
import struct Foundation.URL

Expand Down Expand Up @@ -50,14 +51,22 @@ func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
)
log("Found base image configuration: \(baseImageManifest.config.digest)")

// Honour SOURCE_DATE_EPOCH for reproducible builds if set.
let sourceEpoch: Int
if let epochStr = ProcessInfo.processInfo.environment["SOURCE_DATE_EPOCH"],
let epochValue = Int(epochStr) {
sourceEpoch = epochValue
} else {
sourceEpoch = 0
}
// MARK: Upload resource layers

var resourceLayers: [(descriptor: ContentDescriptor, diffID: ImageReference.Digest)] = []
for resourceDir in resources {
let paths = resourceDir.split(separator: ":", maxSplits: 1)
switch paths.count {
case 1:
let resourceTardiff = try Archive().appendingRecursively(atPath: resourceDir).bytes
let resourceTardiff = try Archive().appendingRecursively(atPath: resourceDir, mtime: sourceEpoch).bytes
let resourceLayer = try await destination.uploadLayer(
repository: destinationImage.repository,
contents: resourceTardiff
Expand Down Expand Up @@ -85,7 +94,8 @@ func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
archive = try Archive()
.appendingFile(
at: URL(fileURLWithPath: String(sourcePath)),
to: URL(fileURLWithPath: String(destinationPath))
to: URL(fileURLWithPath: String(destinationPath)),
mtime: sourceEpoch
)
}

Expand All @@ -108,15 +118,15 @@ func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(

let applicationLayer = try await destination.uploadLayer(
repository: destinationImage.repository,
contents: try Archive().appendingFile(at: executableURL).bytes
contents: try Archive().appendingFile(at: executableURL, mtime: sourceEpoch).bytes
)
if verbose {
log("application layer: \(applicationLayer.descriptor.digest) (\(applicationLayer.descriptor.size) bytes)")
}

// MARK: Create the application configuration

let timestamp = Date(timeIntervalSince1970: 0).ISO8601Format()
let timestamp = Date(timeIntervalSince1970: TimeInterval(sourceEpoch)).ISO8601Format()

// Inherit the configuration of the base image - UID, GID, environment etc -
// and override the entrypoint.
Expand Down
Loading