diff --git a/Plugins/ContainerImageBuilder/main.swift b/Plugins/ContainerImageBuilder/main.swift index 64a06e1..b842633 100644 --- a/Plugins/ContainerImageBuilder/main.swift +++ b/Plugins/ContainerImageBuilder/main.swift @@ -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() diff --git a/Sources/Tar/tar.swift b/Sources/Tar/tar.swift index 62a17b1..248d4d9 100644 --- a/Sources/Tar/tar.swift +++ b/Sources/Tar/tar.swift @@ -432,8 +432,8 @@ 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)) } /// Adds a new file member at the end of the archive @@ -441,23 +441,23 @@ extension Archive { /// - 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))) } } diff --git a/Sources/containertool/Extensions/Archive+appending.swift b/Sources/containertool/Extensions/Archive+appending.swift index 2f2acc7..dd67da6 100644 --- a/Sources/containertool/Extensions/Archive+appending.swift +++ b/Sources/containertool/Extensions/Archive+appending.swift @@ -20,80 +20,83 @@ import struct Foundation.URL import Tar extension URL { - var isDirectory: Bool { - (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true - } + var isDirectory: Bool { + (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.. 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.. 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 - } } diff --git a/Sources/containertool/Extensions/RegistryClient+publish.swift b/Sources/containertool/Extensions/RegistryClient+publish.swift index 84b5f50..7d93c5a 100644 --- a/Sources/containertool/Extensions/RegistryClient+publish.swift +++ b/Sources/containertool/Extensions/RegistryClient+publish.swift @@ -14,6 +14,7 @@ import class Foundation.FileManager import struct Foundation.ObjCBool +import class Foundation.ProcessInfo import struct Foundation.Date import struct Foundation.URL @@ -50,6 +51,14 @@ func publishContainerImage( ) 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)] = [] @@ -57,7 +66,7 @@ func publishContainerImage( 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 @@ -85,7 +94,8 @@ func publishContainerImage( archive = try Archive() .appendingFile( at: URL(fileURLWithPath: String(sourcePath)), - to: URL(fileURLWithPath: String(destinationPath)) + to: URL(fileURLWithPath: String(destinationPath)), + mtime: sourceEpoch ) } @@ -108,7 +118,7 @@ func publishContainerImage( 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)") @@ -116,7 +126,7 @@ func publishContainerImage( // 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.