feat: add extraction backend preference (native/system xip)

This commit is contained in:
Stijn Willems 2026-03-17 10:32:07 +01:00
parent 2387d96d95
commit a7d500ade2
No known key found for this signature in database
3 changed files with 38 additions and 18 deletions

View file

@ -251,7 +251,7 @@ extension AppState {
func unarchiveAndMoveXIP(availableXcode: AvailableXcode, at source: URL, to destination: URL) -> AnyPublisher<URL, Swift.Error> {
self.setInstallationStep(of: availableXcode.version, to: .unarchiving)
return unxipOrUnxipExperiment(source)
return extractXIP(source)
.catch { error -> AnyPublisher<ProcessOutput, Swift.Error> in
if let executionError = error as? ProcessExecutionError {
if executionError.standardError.contains("damaged and cant be expanded") {
@ -291,13 +291,14 @@ extension AppState {
.eraseToAnyPublisher()
}
func unxipOrUnxipExperiment(_ source: URL) -> AnyPublisher<ProcessOutput, Error> {
if unxipExperiment {
func extractXIP(_ source: URL) -> AnyPublisher<ProcessOutput, Error> {
switch extractionBackend {
case .nativeLibunxip:
// Native libunxip integration - no subprocess needed
// https://github.com/saagarjha/unxip
let destination = source.deletingLastPathComponent()
return Current.shell.unxipNative(source, destination)
} else {
case .systemXip:
return Current.shell.unxip(source)
}
}

View file

@ -14,7 +14,7 @@ import LibFido2Swift
enum PreferenceKey: String {
case installPath
case localPath
case unxipExperiment
case extractionBackend
case createSymLinkOnSelect
case onSelectActionType
case showOpenInRosettaOption
@ -31,6 +31,23 @@ enum PreferenceKey: String {
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
}
/// Controls which backend is used for extracting .xip archives.
enum ExtractionBackend: String, CaseIterable, Identifiable {
/// Uses /usr/bin/xip --expand (slowest, always works)
case systemXip = "system"
/// Uses libunxip natively (fastest, built-in)
case nativeLibunxip = "native"
var id: Self { self }
var displayName: String {
switch self {
case .systemXip: return "System xip (slowest, most compatible)"
case .nativeLibunxip: return "Native libunxip (fastest)"
}
}
}
class AppState: ObservableObject {
private let client = AppleAPI.Client()
internal let runtimeService = RuntimeService()
@ -97,13 +114,13 @@ class AppState: ObservableObject {
var disableInstallPathChange: Bool { PreferenceKey.installPath.isManaged() }
@Published var unxipExperiment = false {
@Published var extractionBackend: ExtractionBackend = .nativeLibunxip {
didSet {
Current.defaults.set(unxipExperiment, forKey: "unxipExperiment")
Current.defaults.set(extractionBackend.rawValue, forKey: "extractionBackend")
}
}
var disableUnxipExperiment: Bool { PreferenceKey.unxipExperiment.isManaged() }
var disableExtractionBackendChange: Bool { PreferenceKey.extractionBackend.isManaged() }
@Published var createSymLinkOnSelect = false {
didSet {
@ -203,7 +220,7 @@ class AppState: ObservableObject {
func setupDefaults() {
localPath = Current.defaults.string(forKey: "localPath") ?? Path.defaultXcodesApplicationSupport.string
unxipExperiment = Current.defaults.bool(forKey: "unxipExperiment") ?? false
extractionBackend = ExtractionBackend(rawValue: Current.defaults.string(forKey: "extractionBackend") ?? "") ?? .nativeLibunxip
createSymLinkOnSelect = Current.defaults.bool(forKey: "createSymLinkOnSelect") ?? false
onSelectActionType = SelectedActionType(rawValue: Current.defaults.string(forKey: "onSelectActionType") ?? "none") ?? .none
installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string

View file

@ -4,17 +4,19 @@ import SwiftUI
struct ExperimentsPreferencePane: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack(alignment: .leading, spacing: 20) {
GroupBox(label: Text("FasterUnxip")) {
GroupBox(label: Text("ExtractionBackend")) {
VStack(alignment: .leading) {
Toggle(
"UseUnxipExperiment",
isOn: $appState.unxipExperiment
)
.disabled(appState.disableUnxipExperiment)
Text("FasterUnxipDescription")
Picker("ExtractionBackendPicker", selection: $appState.extractionBackend) {
ForEach(ExtractionBackend.allCases) { backend in
Text(backend.displayName).tag(backend)
}
}
.pickerStyle(.radioGroup)
.disabled(appState.disableExtractionBackendChange)
Text("ExtractionBackendDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)