From b1a367809dfde617a217a62596a5e4da37c370de Mon Sep 17 00:00:00 2001 From: Stijn Willems Date: Tue, 17 Mar 2026 10:32:01 +0100 Subject: [PATCH] refactor: replace Process-based unxip with libunxip async API --- Xcodes/Backend/AppState+Install.swift | 7 ++-- Xcodes/Backend/Environment.swift | 50 +++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 5e2a074..6f95df7 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -293,9 +293,10 @@ extension AppState { func unxipOrUnxipExperiment(_ source: URL) -> AnyPublisher { if unxipExperiment { - // All hard work done by https://github.com/saagarjha/unxip - // Compiled to binary with `swiftc -parse-as-library -O unxip.swift` - return Current.shell.unxipExperiment(source) + // Native libunxip integration - no subprocess needed + // https://github.com/saagarjha/unxip + let destination = source.deletingLastPathComponent() + return Current.shell.unxipNative(source, destination) } else { return Current.shell.unxip(source) } diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index b515e11..0bbc70b 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -4,6 +4,7 @@ import Path import AppleAPI import KeychainAccess import XcodesKit +import libunxip /** Lightweight dependency injection using global mutable state :P @@ -191,9 +192,52 @@ public struct Shell { } - public var unxipExperiment: (URL) -> AnyPublisher = { url in - let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)! - return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"]) + /// Extracts a .xip archive using libunxip natively (no subprocess). + /// The source URL is the .xip file path; the destination URL is the output directory. + public var unxipNative: (URL, URL) -> AnyPublisher = { source, destination in + Future { promise in + Task { + do { + let handle = try FileHandle(forReadingFrom: source) + defer { try? handle.close() } + + // Open the output directory as a file descriptor for libunxip + let outputFd = open(destination.path, O_RDONLY | O_DIRECTORY) + guard outputFd >= 0 else { + throw NSError( + domain: NSPOSIXErrorDomain, + code: Int(errno), + userInfo: [NSLocalizedDescriptionKey: "Failed to open output directory: \(destination.path)"] + ) + } + defer { close(outputFd) } + + let data = DataReader(descriptor: handle.fileDescriptor) + let filesOptions = libunxip.Files.Options( + compress: true, + dryRun: false, + output: outputFd + ) + + // Run the full XIP -> Chunks -> Files -> Disk pipeline + for try await _ in Unxip.makeStream( + from: .xip(), + to: .disk(), + input: data, + nil, + nil, + filesOptions + ) { + // Each yielded File has been written to disk + } + + promise(.success((0, "", ""))) + } catch { + promise(.failure(error)) + } + } + } + .eraseToAnyPublisher() } public var downloadRuntime: (String, String, String?) -> AsyncThrowingStream = { platform, version, architecture in