diff --git a/Plugins/ContainerImageBuilder/main.swift b/Plugins/ContainerImageBuilder/main.swift index 434621c..64a06e1 100644 --- a/Plugins/ContainerImageBuilder/main.swift +++ b/Plugins/ContainerImageBuilder/main.swift @@ -48,6 +48,7 @@ extension PluginError: CustomStringConvertible { OPTIONS --product Product to include in the image + --resources Directory of resources to include in the image Other arguments are passed to the containertool helper. """ @@ -89,16 +90,29 @@ extension PluginError: CustomStringConvertible { for built in builtExecutables { Diagnostics.remark("Built product: \(built.url.path)") } - let resources = builtExecutables[0].url + let resourcesBundle = builtExecutables[0].url .deletingLastPathComponent() .appendingPathComponent( "\(context.package.displayName)_\(productName).resources" ) + var resources: [String] = [] + if FileManager.default.fileExists(atPath: resourcesBundle.path) { + resources.append(resourcesBundle.path) + } + + for resource in extractor.extractOption(named: "resources") { + let paths = resource.split(separator: ":", maxSplits: 1) + if paths.count >= 1 && !FileManager.default.fileExists(atPath: String(paths[0])) { + throw PluginError.argumentError("Resource directory \(resource) does not exist") + } + resources.append(resource) + } + // Run a command line helper to upload the image let helperURL = try context.tool(named: "containertool").url let helperArgs = - (FileManager.default.fileExists(atPath: resources.path) ? ["--resources", resources.path] : []) + resources.map { "--resources=\($0)" } + builtExecutables.map { $0.url.path } + extractor.remainingArguments let helperEnv = ProcessInfo.processInfo.environment.filter { $0.key.starts(with: "CONTAINERTOOL_") } diff --git a/Sources/containertool/Extensions/Archive+appending.swift b/Sources/containertool/Extensions/Archive+appending.swift index 76d7480..2f2acc7 100644 --- a/Sources/containertool/Extensions/Archive+appending.swift +++ b/Sources/containertool/Extensions/Archive+appending.swift @@ -47,11 +47,25 @@ extension Archive { try self.appendingFile(name: path.lastPathComponent, data: try [UInt8](Data(contentsOf: path))) } + 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.. Self { + 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 { @@ -66,6 +80,7 @@ extension Archive { 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))) diff --git a/Sources/containertool/Extensions/RegistryClient+publish.swift b/Sources/containertool/Extensions/RegistryClient+publish.swift index 8a6864c..32d2241 100644 --- a/Sources/containertool/Extensions/RegistryClient+publish.swift +++ b/Sources/containertool/Extensions/RegistryClient+publish.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import class Foundation.FileManager +import struct Foundation.ObjCBool import struct Foundation.Date import struct Foundation.URL @@ -25,6 +27,7 @@ func publishContainerImage( destination: Destination, architecture: String, os: String, + entrypoint: String?, cmd: [String], resources: [String], tag: String?, @@ -50,17 +53,54 @@ func publishContainerImage( var resourceLayers: [(descriptor: ContentDescriptor, diffID: ImageReference.Digest)] = [] for resourceDir in resources { - let resourceTardiff = try Archive().appendingRecursively(atPath: resourceDir).bytes - let resourceLayer = try await destination.uploadLayer( - repository: destinationImage.repository, - contents: resourceTardiff - ) + let paths = resourceDir.split(separator: ":", maxSplits: 1) + switch paths.count { + case 1: + let resourceTardiff = try Archive().appendingRecursively(atPath: resourceDir).bytes + let resourceLayer = try await destination.uploadLayer( + repository: destinationImage.repository, + contents: resourceTardiff + ) - if verbose { - log("resource layer: \(resourceLayer.descriptor.digest) (\(resourceLayer.descriptor.size) bytes)") - } + if verbose { + log("resource layer: \(resourceLayer.descriptor.digest) (\(resourceLayer.descriptor.size) bytes)") + } + + resourceLayers.append(resourceLayer) + case 2: + let sourcePath = paths[0] + let destinationPath = paths[1] + + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: String(sourcePath), isDirectory: &isDirectory) else { + preconditionFailure("Source does not exist: \(source)") + } + + let archive: Archive + if isDirectory.boolValue { + // archive = try Archive().appendingDirectoryTree(at: URL(fileURLWithPath: String(sourcePath))) + preconditionFailure("Directory trees are not supported yet") + } else { + archive = try Archive() + .appendingFile( + at: URL(fileURLWithPath: String(sourcePath)), + to: URL(fileURLWithPath: String(destinationPath)) + ) + } + + let resourceLayer = try await destination.uploadLayer( + repository: destinationImage.repository, + contents: archive.bytes + ) - resourceLayers.append(resourceLayer) + if verbose { + log("resource layer: \(resourceLayer.descriptor.digest) (\(resourceLayer.descriptor.size) bytes)") + } + + resourceLayers.append(resourceLayer) + default: + preconditionFailure("Invalid resource directory: \(resourceDir)") + } } // MARK: Upload the application layer @@ -80,7 +120,11 @@ func publishContainerImage( // Inherit the configuration of the base image - UID, GID, environment etc - // and override the entrypoint. var inheritedConfiguration = baseImageConfiguration.config ?? .init() - inheritedConfiguration.Entrypoint = ["/\(executableURL.lastPathComponent)"] + if let entrypoint { + inheritedConfiguration.Entrypoint = [entrypoint] + } else { + inheritedConfiguration.Entrypoint = ["/\(executableURL.lastPathComponent)"] + } inheritedConfiguration.Cmd = cmd inheritedConfiguration.WorkingDir = "/" diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index 779227d..6733c66 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -65,6 +65,9 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti @Option(help: "Operating system") var os: String? + @Option(help: "Entrypoint process") + var entrypoint: String? + @Option(parsing: .remaining, help: "Default arguments to pass to the entrypoint process") var cmd: [String] = [] } @@ -225,6 +228,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti destination: destination, architecture: architecture, os: os, + entrypoint: imageConfigurationOptions.entrypoint, cmd: imageConfigurationOptions.cmd, resources: imageBuildOptions.resources, tag: repositoryOptions.tag, diff --git a/Vendor/github.com/apple/swift-package-manager/Sources/Basics/AuthorizationProvider.swift b/Vendor/github.com/apple/swift-package-manager/Sources/Basics/AuthorizationProvider.swift index 6769397..f4b5c54 100644 --- a/Vendor/github.com/apple/swift-package-manager/Sources/Basics/AuthorizationProvider.swift +++ b/Vendor/github.com/apple/swift-package-manager/Sources/Basics/AuthorizationProvider.swift @@ -18,7 +18,7 @@ import struct Foundation.Data import struct Foundation.Date import struct Foundation.URL #if canImport(Security) - import Security +import Security #endif public protocol AuthorizationProvider: Sendable { @@ -56,14 +56,14 @@ public final class NetrcAuthorizationProvider: AuthorizationProvider { } public func authentication(for url: URL) -> (user: String, password: String)? { - return self.machine(for: url).map { (user: $0.login, password: $0.password) } + self.machine(for: url).map { (user: $0.login, password: $0.password) } } private func machine(for url: URL) -> Basics.Netrc.Machine? { // Since updates are appended to the end of the file, we // take the _last_ match to use the most recent entry. if let machine = NetrcAuthorizationProvider.machine(for: url), - let existing = self.netrc?.machines.last(where: { $0.name.lowercased() == machine }) + let existing = self.netrc?.machines.last(where: { $0.name.lowercased() == machine }) { return existing } diff --git a/Vendor/github.com/apple/swift-package-manager/Sources/Basics/Netrc.swift b/Vendor/github.com/apple/swift-package-manager/Sources/Basics/Netrc.swift index 0e24842..0dd61ef 100644 --- a/Vendor/github.com/apple/swift-package-manager/Sources/Basics/Netrc.swift +++ b/Vendor/github.com/apple/swift-package-manager/Sources/Basics/Netrc.swift @@ -27,8 +27,10 @@ public struct Netrc: Sendable { /// - Parameters: /// - url: The url to retrieve authorization information for. public func authorization(for url: URL) -> Authorization? { - guard let index = machines.firstIndex(where: { $0.name == url.host }) ?? machines - .firstIndex(where: { $0.isDefault }) + guard + let index = machines.firstIndex(where: { $0.name == url.host }) + ?? machines + .firstIndex(where: { $0.isDefault }) else { return .none } @@ -53,8 +55,10 @@ public struct Netrc: Sendable { } init?(for match: NSTextCheckingResult, string: String, variant: String = "") { - guard let name = RegexUtil.Token.machine.capture(in: match, string: string) ?? RegexUtil.Token.default - .capture(in: match, string: string), + guard + let name = RegexUtil.Token.machine.capture(in: match, string: string) + ?? RegexUtil.Token.default + .capture(in: match, string: string), let login = RegexUtil.Token.login.capture(prefix: variant, in: match, string: string), let password = RegexUtil.Token.password.capture(prefix: variant, in: match, string: string) else { @@ -87,11 +91,12 @@ public struct NetrcParser { let matches = regex.matches( in: content, options: [], - range: NSRange(content.startIndex ..< content.endIndex, in: content) + range: NSRange(content.startIndex..