From 7645ca7e788a05e1f37bf2308572d0aa39df19db Mon Sep 17 00:00:00 2001 From: lakshitsoni26 Date: Thu, 9 Jul 2026 00:15:34 +0530 Subject: [PATCH] Fix build context symlink resolution and XPC concurrency --- Makefile | 4 +- Sources/ContainerBuild/BuildFSSync.swift | 16 +--- Sources/ContainerBuild/Builder.swift | 4 +- Sources/ContainerBuild/URL+Extensions.swift | 19 +++-- Sources/ContainerCommands/BuildCommand.swift | 9 ++- Sources/ContainerXPC/XPCClient.swift | 78 ++++++++++++-------- 6 files changed, 73 insertions(+), 57 deletions(-) diff --git a/Makefile b/Makefile index 1a25241a1..3cc15ffa8 100644 --- a/Makefile +++ b/Makefile @@ -285,9 +285,9 @@ define RUN_INTEGRATION echo "==> Warmup pass" && \ $(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter "$(WARMUP_FILTER)" && \ echo "==> Concurrent pass (width=$(PARALLEL_WIDTH))" && \ - $(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --experimental-maximum-parallelization-width $(PARALLEL_WIDTH) --filter "$(CONCURRENT_FILTER)" && \ + $(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --parallel --num-workers $(PARALLEL_WIDTH) --filter "$(CONCURRENT_FILTER)" && \ echo "==> Global pass (serial)" && \ - $(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --experimental-maximum-parallelization-width 1 --filter "$(SERIAL_FILTER)" ; \ + $(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --no-parallel --filter "$(SERIAL_FILTER)" ; \ exit_code=$$? ; \ $(INTEGRATION_POST_TEST) \ echo Ensuring apiserver stopped after the CLI integration tests ; \ diff --git a/Sources/ContainerBuild/BuildFSSync.swift b/Sources/ContainerBuild/BuildFSSync.swift index 7b4fc4400..5baff5826 100644 --- a/Sources/ContainerBuild/BuildFSSync.swift +++ b/Sources/ContainerBuild/BuildFSSync.swift @@ -151,15 +151,6 @@ actor BuildFSSync: BuildPipelineHandler { let entry = DirEntry(url: url, isDirectory: url.hasDirectoryPath, relativePath: relPath) entries[parentPath, default: []].insert(entry) - if url.isSymlink { - let target: URL = url.resolvingSymlinksInPath() - if self.contextDir.parentOf(target) { - let relPath = try target.relativeChildPath(to: self.contextDir) - let entry = DirEntry(url: target, isDirectory: target.hasDirectoryPath, relativePath: relPath) - let parentPath: String = try target.deletingLastPathComponent().relativeChildPath(to: self.contextDir) - entries[parentPath, default: []].insert(entry) - } - } } var fileOrder = [String]() @@ -335,12 +326,7 @@ actor BuildFSSync: BuildPipelineHandler { init(path: URL, contextDir: URL) throws { if path.isSymlink { - let target: URL = path.resolvingSymlinksInPath() - if contextDir.parentOf(target) { - self.target = target.relativePathFrom(from: path) - } else { - self.target = target.cleanPath - } + self.target = try FileManager.default.destinationOfSymbolicLink(atPath: path.cleanPath) } else { self.target = "" } diff --git a/Sources/ContainerBuild/Builder.swift b/Sources/ContainerBuild/Builder.swift index c43b5ea63..a8219b75f 100644 --- a/Sources/ContainerBuild/Builder.swift +++ b/Sources/ContainerBuild/Builder.swift @@ -222,7 +222,9 @@ public struct Builder: Sendable { switch type { case "oci", "tar", "local": - break // ignore destination + if let destination = destination { + components.append("dest=\(destination.path(percentEncoded: false))") + } default: throw Builder.Error.invalidExport(rawValue, "unsupported output type") } diff --git a/Sources/ContainerBuild/URL+Extensions.swift b/Sources/ContainerBuild/URL+Extensions.swift index 4f0bd7ec1..607139748 100644 --- a/Sources/ContainerBuild/URL+Extensions.swift +++ b/Sources/ContainerBuild/URL+Extensions.swift @@ -54,9 +54,16 @@ extension URL { self.path.fs_cleaned } + private func stripPrivatePrefix(_ path: String) -> String { + if path.hasPrefix("/private/") { + return String(path.dropFirst(8)) + } + return path + } + func parentOf(_ url: URL) -> Bool { - let parentPath = self.absoluteURL.cleanPath - let childPath = url.absoluteURL.cleanPath + let parentPath = stripPrivatePrefix(self.absoluteURL.cleanPath) + let childPath = stripPrivatePrefix(url.absoluteURL.cleanPath) guard parentPath.fs_isAbsolute else { return true @@ -74,15 +81,15 @@ extension URL { throw BuildFSSync.Error.pathIsNotChild(cleanPath, context.cleanPath) } - let ctxParts = context.cleanPath.fs_components - let selfParts = cleanPath.fs_components + let ctxParts = stripPrivatePrefix(context.absoluteURL.cleanPath).fs_components + let selfParts = stripPrivatePrefix(self.absoluteURL.cleanPath).fs_components return selfParts.dropFirst(ctxParts.count).joined(separator: "/") } func relativePathFrom(from base: URL) -> String { - let destParts = cleanPath.fs_components - let baseParts = base.cleanPath.fs_components + let destParts = stripPrivatePrefix(self.absoluteURL.cleanPath).fs_components + let baseParts = stripPrivatePrefix(base.absoluteURL.cleanPath).fs_components let common = zip(destParts, baseParts).prefix { $0 == $1 }.count guard common > 0 else { return cleanPath } diff --git a/Sources/ContainerCommands/BuildCommand.swift b/Sources/ContainerCommands/BuildCommand.swift index 34ec4356f..4ff73fda5 100644 --- a/Sources/ContainerCommands/BuildCommand.swift +++ b/Sources/ContainerCommands/BuildCommand.swift @@ -245,10 +245,15 @@ extension Application { } try fileHandle.close() buildFileData = try Data(contentsOf: URL(filePath: tempFile.path())) + + let contextIgnoreURL = URL(fileURLWithPath: contextDir).appendingPathComponent(".dockerignore") + ignoreFileData = try? Data(contentsOf: contextIgnoreURL) } else { - let ignoreFileURL = URL(filePath: dockerfile + ".dockerignore") + let specificIgnoreURL = URL(filePath: dockerfile + ".dockerignore") + let contextIgnoreURL = URL(fileURLWithPath: contextDir).appendingPathComponent(".dockerignore") + buildFileData = try Data(contentsOf: URL(filePath: dockerfile)) - ignoreFileData = try? Data(contentsOf: ignoreFileURL) + ignoreFileData = (try? Data(contentsOf: specificIgnoreURL)) ?? (try? Data(contentsOf: contextIgnoreURL)) } // BUG: See https://github.com/apple/container/issues/735. diff --git a/Sources/ContainerXPC/XPCClient.swift b/Sources/ContainerXPC/XPCClient.swift index bab008509..844cb5910 100644 --- a/Sources/ContainerXPC/XPCClient.swift +++ b/Sources/ContainerXPC/XPCClient.swift @@ -95,45 +95,61 @@ extension XPCClient { XPCClientSession(client: self) } + private actor ContinuationResolver { + var continuation: CheckedContinuation? + init(_ continuation: CheckedContinuation) { + self.continuation = continuation + } + func resume(returning value: XPCMessage) { + continuation?.resume(returning: value) + continuation = nil + } + func resume(throwing error: Error) { + continuation?.resume(throwing: error) + continuation = nil + } + } + /// Send the provided message to the service. @discardableResult public func send(_ message: XPCMessage, responseTimeout: Duration? = nil) async throws -> XPCMessage { - try await withThrowingTaskGroup(of: XPCMessage.self, returning: XPCMessage.self) { group in - if let responseTimeout { - group.addTask { - try await Task.sleep(for: responseTimeout) - let route = message.string(key: XPCMessage.routeKey) ?? "nil" - throw ContainerizationError( - .internalError, - message: "XPC timeout for request to \(self.service)/\(route)" - ) + try await withCheckedThrowingContinuation { cont in + let resolver = ContinuationResolver(cont) + + let timeoutTask = responseTimeout.map { timeout in + Task { + do { + try await Task.sleep(for: timeout) + let route = message.string(key: XPCMessage.routeKey) ?? "nil" + await resolver.resume(throwing: ContainerizationError( + .internalError, + message: "XPC timeout for request to \(self.service)/\(route)" + )) + } catch { + // Task was cancelled before timeout elapsed + } } } - - group.addTask { - try await withCheckedThrowingContinuation { cont in - xpc_connection_send_message_with_reply(self.connection, message.underlying, nil) { reply in - do { - let message = try self.parseReply(reply) - cont.resume(returning: message) - } catch { - cont.resume(throwing: error) - } + + xpc_connection_send_message_with_reply(self.connection, message.underlying, nil) { reply in + timeoutTask?.cancel() + + let parsedResult: Result + do { + let parsedMessage = try self.parseReply(reply) + parsedResult = .success(parsedMessage) + } catch { + parsedResult = .failure(error) + } + Task { + switch parsedResult { + case .success(let parsedMessage): + await resolver.resume(returning: parsedMessage) + case .failure(let error): + await resolver.resume(throwing: error) } } } - - let response = try await group.next() - // once one task has finished, cancel the rest. - group.cancelAll() - // we don't really care about the second error here - // as it's most likely a `CancellationError`. - try? await group.waitForAll() - - guard let response else { - throw ContainerizationError(.invalidState, message: "failed to receive XPC response") - } - return response } }