mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-26 09:05:46 +00:00
Use Privileged Helper for file operations
Enable Xcodes.app to be used as a Standard (ie non Admin) user in managed environments. Install, rename/symbolic link to Xcode.app during activate/switch, uninstall are performed using the helper. An options in the Advanced Preferences pane added to enable/disable this feature.
This commit is contained in:
parent
17f3d365b8
commit
b77830cff0
14 changed files with 487 additions and 129 deletions
2
App.xcconfig
Normal file
2
App.xcconfig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DEVELOPMENT_TEAM=ZU6GR6B2FY
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT=ZU6GR6B2FY
|
||||
2
Helper.xcconfig
Normal file
2
Helper.xcconfig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DEVELOPMENT_TEAM=ZU6GR6B2FY
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT=ZU6GR6B2FY
|
||||
61
HelperXPCShared/FileOperations.swift
Normal file
61
HelperXPCShared/FileOperations.swift
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Foundation
|
||||
import os.log
|
||||
|
||||
enum FileOperations {
|
||||
private static var subsystem = Bundle.main.bundleIdentifier!
|
||||
static let fileOperations = Logger(subsystem: subsystem, category: "fileOperations")
|
||||
|
||||
static func moveApp(at source: String, to destination: String, completion: @escaping ((any Error)?) -> Void) {
|
||||
do {
|
||||
guard URL(fileURLWithPath: source).hasDirectoryPath else { throw XPCDelegateError(.invalidSourcePath)}
|
||||
|
||||
guard URL(fileURLWithPath: destination).deletingLastPathComponent().hasDirectoryPath else { throw
|
||||
XPCDelegateError(.invalidDestinationPath)}
|
||||
|
||||
try FileManager.default.moveItem(at: URL(fileURLWithPath: source), to: URL(fileURLWithPath: destination))
|
||||
completion(nil)
|
||||
} catch {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
|
||||
// does an Xcode.app file exist?
|
||||
static func createSymbolicLink(source: String, destination: String, completion: @escaping ((any Error)?) -> Void) {
|
||||
do {
|
||||
if FileManager.default.fileExists(atPath: destination) {
|
||||
let attributes: [FileAttributeKey : Any]? = try? FileManager.default.attributesOfItem(atPath: destination)
|
||||
|
||||
if attributes?[.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink {
|
||||
try FileManager.default.removeItem(atPath: destination)
|
||||
Self.fileOperations.info("Successfully deleted old symlink")
|
||||
} else {
|
||||
throw XPCDelegateError(.destinationIsNotASymbolicLink)
|
||||
}
|
||||
}
|
||||
|
||||
try FileManager.default.createSymbolicLink(atPath: destination, withDestinationPath: source)
|
||||
Self.fileOperations.info("Successfully created symbolic link with \(destination)")
|
||||
completion(nil)
|
||||
} catch {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
|
||||
static func rename(source: String, destination: String, completion: @escaping ((any Error)?) -> Void) {
|
||||
do {
|
||||
try FileManager.default.moveItem(at: URL(fileURLWithPath: source), to: URL(fileURLWithPath: destination))
|
||||
completion(nil)
|
||||
} catch {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
|
||||
static func remove(path: String, completion: @escaping ((any Error)?) -> Void) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: path)
|
||||
completion(nil)
|
||||
} catch {
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,4 +12,54 @@ protocol HelperXPCProtocol {
|
|||
func addStaffToDevelopersGroup(completion: @escaping (Error?) -> Void)
|
||||
func acceptXcodeLicense(absoluteXcodePath: String, completion: @escaping (Error?) -> Void)
|
||||
func runFirstLaunch(absoluteXcodePath: String, completion: @escaping (Error?) -> Void)
|
||||
func moveApp(at source: String, to destination: String, completion: @escaping (Error?) -> Void)
|
||||
func createSymbolicLink(source: String, destination: String, completion: @escaping (Error?) -> Void)
|
||||
func rename(source: String, destination: String, completion: @escaping (Error?) -> Void)
|
||||
func remove(path: String, completion: @escaping (Error?) -> Void)
|
||||
}
|
||||
|
||||
struct XPCDelegateError: CustomNSError {
|
||||
enum Code: Int {
|
||||
case invalidXcodePath
|
||||
case invalidSourcePath
|
||||
case invalidDestinationPath
|
||||
case destinationIsNotASymbolicLink
|
||||
}
|
||||
|
||||
let code: Code
|
||||
|
||||
init(_ code: Code) {
|
||||
self.code = code
|
||||
}
|
||||
|
||||
// MARK: - CustomNSError
|
||||
|
||||
static var errorDomain: String { "XPCDelegateError" }
|
||||
|
||||
var errorCode: Int { code.rawValue }
|
||||
|
||||
var errorUserInfo: [String : Any] {
|
||||
switch code {
|
||||
case .invalidXcodePath:
|
||||
return [
|
||||
NSLocalizedDescriptionKey: "Invalid Xcode path.",
|
||||
NSLocalizedFailureReasonErrorKey: "Xcode path must be absolute."
|
||||
]
|
||||
case .invalidSourcePath:
|
||||
return [
|
||||
NSLocalizedDescriptionKey: "Invalid source path.",
|
||||
NSLocalizedFailureReasonErrorKey: "Source path must be absolute and must be a directory."
|
||||
]
|
||||
case .invalidDestinationPath:
|
||||
return [
|
||||
NSLocalizedDescriptionKey: "Invalid destination path.",
|
||||
NSLocalizedFailureReasonErrorKey: "Destination path must be absolute and must be a directory."
|
||||
]
|
||||
case .destinationIsNotASymbolicLink:
|
||||
return [
|
||||
NSLocalizedDescriptionKey: "Invalid destination path.",
|
||||
NSLocalizedFailureReasonErrorKey: "Destination path must be a symbolic link."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
Tests.xcconfig
Normal file
2
Tests.xcconfig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DEVELOPMENT_TEAM=ZU6GR6B2FY
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT=ZU6GR6B2FY
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
150235A12CED5E2200F6ECBF /* FileOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 150235A02CED5E2200F6ECBF /* FileOperations.swift */; };
|
||||
150235A22CED5E2200F6ECBF /* FileOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 150235A02CED5E2200F6ECBF /* FileOperations.swift */; };
|
||||
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; };
|
||||
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; };
|
||||
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
|
||||
|
|
@ -197,6 +199,10 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
150235A02CED5E2200F6ECBF /* FileOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperations.swift; sourceTree = "<group>"; };
|
||||
1542A3022CEF05AE00DB71B0 /* App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = App.xcconfig; sourceTree = "<group>"; };
|
||||
1542A3032CEF05B800DB71B0 /* Tests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tests.xcconfig; sourceTree = "<group>"; };
|
||||
1542A3042CEF05C900DB71B0 /* Helper.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Helper.xcconfig; sourceTree = "<group>"; };
|
||||
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
|
||||
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = "<group>"; };
|
||||
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -456,6 +462,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */,
|
||||
150235A02CED5E2200F6ECBF /* FileOperations.swift */,
|
||||
);
|
||||
path = HelperXPCShared;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -593,6 +600,9 @@
|
|||
CAD2E79F2449574E00113D76 /* Products */,
|
||||
CA538A12255A4F7C00E64DD7 /* Frameworks */,
|
||||
CA452BE025A2354D0072DFA4 /* Recovered References */,
|
||||
1542A3022CEF05AE00DB71B0 /* App.xcconfig */,
|
||||
1542A3032CEF05B800DB71B0 /* Tests.xcconfig */,
|
||||
1542A3042CEF05C900DB71B0 /* Helper.xcconfig */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -884,6 +894,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
150235A22CED5E2200F6ECBF /* FileOperations.swift in Sources */,
|
||||
CA9FF8D025959A9700E47BAF /* HelperXPCShared.swift in Sources */,
|
||||
CA42DD7325AEB04300BC0B0C /* Logger.swift in Sources */,
|
||||
CA9FF8DB25959B4000E47BAF /* XPCDelegate.swift in Sources */,
|
||||
|
|
@ -910,6 +921,7 @@
|
|||
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
||||
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
||||
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
||||
150235A12CED5E2200F6ECBF /* FileOperations.swift in Sources */,
|
||||
CAA8587C25A2B37900ACF8C0 /* IsTesting.swift in Sources */,
|
||||
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
|
||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */,
|
||||
|
|
@ -1058,7 +1070,6 @@
|
|||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
|
|
@ -1091,9 +1102,9 @@
|
|||
};
|
||||
CA8FB636256E154800469DA5 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3022CEF05AE00DB71B0 /* App.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/XcodesTest.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
|
|
@ -1120,6 +1131,7 @@
|
|||
};
|
||||
CA8FB637256E154800469DA5 /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3032CEF05B800DB71B0 /* Tests.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
|
|
@ -1144,12 +1156,11 @@
|
|||
};
|
||||
CA9FF8B22595967A00E47BAF /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3042CEF05C900DB71B0 /* Helper.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
|
|
@ -1169,9 +1180,9 @@
|
|||
};
|
||||
CA9FF8B32595967A00E47BAF /* Test */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3042CEF05C900DB71B0 /* Helper.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
CODE_SIGN_ENTITLEMENTS = com.xcodesorg.xcodesapp.Helper/com.xcodesorg.xcodesapp.HelperTest.entitlements;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
||||
|
|
@ -1196,13 +1207,12 @@
|
|||
};
|
||||
CA9FF8B42595967A00E47BAF /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3042CEF05C900DB71B0 /* Helper.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
|
|
@ -1253,7 +1263,6 @@
|
|||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
|
|
@ -1317,7 +1326,6 @@
|
|||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
|
@ -1343,16 +1351,15 @@
|
|||
};
|
||||
CAD2E7BD2449575100113D76 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3022CEF05AE00DB71B0 /* App.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = Xcodes/Resources/Info.plist;
|
||||
|
|
@ -1371,16 +1378,15 @@
|
|||
};
|
||||
CAD2E7BE2449575100113D76 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3022CEF05AE00DB71B0 /* App.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = Xcodes/Resources/Info.plist;
|
||||
|
|
@ -1399,12 +1405,12 @@
|
|||
};
|
||||
CAD2E7C02449575100113D76 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3032CEF05B800DB71B0 /* Tests.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||
INFOPLIST_FILE = XcodesTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -1421,13 +1427,13 @@
|
|||
};
|
||||
CAD2E7C12449575100113D76 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1542A3032CEF05B800DB71B0 /* Tests.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||
INFOPLIST_FILE = XcodesTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
|
|
|||
|
|
@ -266,18 +266,27 @@ extension AppState {
|
|||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.tryMap { output -> URL in
|
||||
.flatMap { output -> AnyPublisher<URL, Swift.Error> in
|
||||
self.setInstallationStep(of: availableXcode.version, to: .moving(destination: destination.path))
|
||||
|
||||
let xcodeURL = source.deletingLastPathComponent().appendingPathComponent("Xcode.app")
|
||||
let xcodeBetaURL = source.deletingLastPathComponent().appendingPathComponent("Xcode-beta.app")
|
||||
if Current.files.fileExists(atPath: xcodeURL.path) {
|
||||
try Current.files.moveItem(at: xcodeURL, to: destination)
|
||||
}
|
||||
else if Current.files.fileExists(atPath: xcodeBetaURL.path) {
|
||||
try Current.files.moveItem(at: xcodeBetaURL, to: destination)
|
||||
return Current.helper.moveApp(xcodeURL.path, destination.path)
|
||||
.map { _ in destination }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
else if Current.files.fileExists(atPath: xcodeBetaURL.path) {
|
||||
return Current.helper.moveApp(xcodeBetaURL.path, destination.path)
|
||||
.map { _ in destination }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Fail(error: InstallationError.failedToMoveXcodeToApplications)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.tryMap { output -> URL in
|
||||
return destination
|
||||
}
|
||||
.handleEvents(receiveCancel: {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ enum PreferenceKey: String {
|
|||
case xcodeListCategory
|
||||
case allowedMajorVersions
|
||||
case hideSupportXcodes
|
||||
case usePrivilegeHelperForFileOperations
|
||||
|
||||
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
|
||||
}
|
||||
|
|
@ -69,7 +70,14 @@ class AppState: ObservableObject {
|
|||
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
|
||||
@Published var presentedAlert: XcodesAlert?
|
||||
@Published var presentedPreferenceAlert: XcodesPreferencesAlert?
|
||||
|
||||
@Published var helperInstallState: HelperInstallState = .notInstalled
|
||||
@Published var usePrivilegedHelperForFileOperations: Bool = false {
|
||||
didSet {
|
||||
Current.defaults.set(usePrivilegedHelperForFileOperations, forKey: PreferenceKey.usePrivilegeHelperForFileOperations.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the user is being prepared for the helper installation alert with an explanation.
|
||||
/// This closure will be performed after the user chooses whether or not to proceed.
|
||||
@Published var isPreparingUserForActionRequiringHelper: ((Bool) -> Void)?
|
||||
|
|
@ -150,6 +158,7 @@ class AppState: ObservableObject {
|
|||
internal var runtimePublishers: [String: Task<(), any Error>] = [:]
|
||||
private var selectPublisher: AnyCancellable?
|
||||
private var uninstallPublisher: AnyCancellable?
|
||||
private var createSymLinkPublisher: AnyCancellable?
|
||||
private var autoInstallTimer: Timer?
|
||||
|
||||
// MARK: - Dock Progress Tracking
|
||||
|
|
@ -702,23 +711,26 @@ class AppState: ObservableObject {
|
|||
return
|
||||
}
|
||||
|
||||
guard
|
||||
var installedXcodePath = xcode.installedPath,
|
||||
selectPublisher == nil
|
||||
else { return }
|
||||
|
||||
if onSelectActionType == .rename {
|
||||
guard let newDestinationXcodePath = renameToXcode(xcode: xcode) else { return }
|
||||
installedXcodePath = newDestinationXcodePath
|
||||
}
|
||||
guard selectPublisher == nil else { return }
|
||||
|
||||
selectPublisher = installHelperIfNecessary()
|
||||
.flatMap {
|
||||
Current.helper.switchXcodePath(installedXcodePath.string)
|
||||
.flatMap { [unowned self] _ -> AnyPublisher<String, Error> in
|
||||
if onSelectActionType == .rename {
|
||||
return self.renameToXcode(xcode: xcode)
|
||||
}
|
||||
|
||||
return Future { promise in
|
||||
promise(.success(xcode.installedPath!.string))
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.flatMap { path in
|
||||
Current.helper.switchXcodePath(path)
|
||||
}
|
||||
.flatMap { [unowned self] _ in
|
||||
self.updateSelectedXcodePath()
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { [unowned self] completion in
|
||||
if case let .failure(error) = completion {
|
||||
|
|
@ -769,66 +781,97 @@ class AppState: ObservableObject {
|
|||
guard let installedXcodePath = xcode.installedPath else { return }
|
||||
|
||||
let destinationPath: Path = Path.installDirectory/"Xcode\(isBeta ? "-Beta" : "").app"
|
||||
|
||||
// does an Xcode.app file exist?
|
||||
if FileManager.default.fileExists(atPath: destinationPath.string) {
|
||||
do {
|
||||
// if it's not a symlink, error because we don't want to delete an actual xcode.app file
|
||||
let attributes: [FileAttributeKey : Any]? = try? FileManager.default.attributesOfItem(atPath: destinationPath.string)
|
||||
|
||||
if attributes?[.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink {
|
||||
try FileManager.default.removeItem(atPath: destinationPath.string)
|
||||
Logger.appState.info("Successfully deleted old symlink")
|
||||
} else {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: localizeString("Alert.SymLink.Message"))
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.localizedDescription)
|
||||
|
||||
createSymLinkPublisher = installHelperIfNecessary()
|
||||
.flatMap {
|
||||
Current.helper.createSymbolicLink(installedXcodePath.string, destinationPath.string)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.createSymbolicLink(atPath: destinationPath.string, withDestinationPath: installedXcodePath.string)
|
||||
Logger.appState.info("Successfully created symbolic link with Xcode\(isBeta ? "-Beta": "").app")
|
||||
} catch {
|
||||
Logger.appState.error("Unable to create symbolic Link")
|
||||
self.error = error
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
|
||||
}
|
||||
.sink(
|
||||
receiveCompletion: { [unowned self] completion in
|
||||
if case let .failure(error) = completion {
|
||||
if let error = error as? CustomNSError {
|
||||
switch error.errorCode {
|
||||
case XPCDelegateError.Code.destinationIsNotASymbolicLink.rawValue:
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: localizeString("Alert.SymLink.Message"))
|
||||
default:
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
|
||||
}
|
||||
} else {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message:
|
||||
error.legibleLocalizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
self.createSymLinkPublisher = nil
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
func renameToXcode(xcode: Xcode) -> Path? {
|
||||
guard let installedXcodePath = xcode.installedPath else { return nil }
|
||||
|
||||
let destinationPath: Path = Path.installDirectory/"Xcode.app"
|
||||
|
||||
// rename any old named `Xcode.app` to the Xcodes versioned named files
|
||||
if FileManager.default.fileExists(atPath: destinationPath.string) {
|
||||
if let originalXcode = Current.files.installedXcode(destination: destinationPath) {
|
||||
let newName = "Xcode-\(originalXcode.version.descriptionWithoutBuildMetadata).app"
|
||||
Logger.appState.debug("Found Xcode.app - renaming back to \(newName)")
|
||||
do {
|
||||
try destinationPath.rename(to: newName)
|
||||
} catch {
|
||||
Logger.appState.error("Unable to create rename Xcode.app back to original")
|
||||
self.error = error
|
||||
// TODO UPDATE MY ERROR STRING
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
|
||||
|
||||
enum RenameXcodeError: Error {
|
||||
case xcodeInstallPathIsNil
|
||||
case xcodeAppNotFound
|
||||
case originalXcodeNameNotFound
|
||||
case renameFailure(Error)
|
||||
}
|
||||
|
||||
func renameToXcode(xcode: Xcode) -> AnyPublisher<String, Error> {
|
||||
guard let installedXcodePath = xcode.installedPath else {
|
||||
return Fail(error: RenameXcodeError.xcodeInstallPathIsNil)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
return Future<String, Error> { promise in
|
||||
let destinationPath: Path = Path.installDirectory/"Xcode.app"
|
||||
// rename any old named `Xcode.app` to the Xcodes versioned named files
|
||||
guard FileManager.default.fileExists(atPath: destinationPath.string) else {
|
||||
promise(.success(destinationPath.string))
|
||||
return
|
||||
}
|
||||
|
||||
guard let originalXcode = Current.files.installedXcode(destination: destinationPath) else {
|
||||
promise(.failure(RenameXcodeError.xcodeAppNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
let newName = "Xcode-\(originalXcode.version.descriptionWithoutBuildMetadata).app"
|
||||
Logger.appState.debug("Found Xcode.app - renaming back to \(newName)")
|
||||
|
||||
Current.helper.rename(destinationPath.string, "\(Path.installDirectory)/\(newName)")
|
||||
.sink { completion in
|
||||
if case let .failure(error) = completion {
|
||||
promise(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
promise(.success(destinationPath.string))
|
||||
} receiveValue: { _ in
|
||||
Void()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
.flatMap { destinationPath in
|
||||
return Future<String, Error> { promise in
|
||||
Current.helper.rename(installedXcodePath.string, destinationPath)
|
||||
.sink { completion in
|
||||
if case let .failure(error) = completion {
|
||||
promise(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
promise(.success(destinationPath))
|
||||
} receiveValue: { _ in
|
||||
Void()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
// rename passed in xcode to xcode.app
|
||||
Logger.appState.debug("Found Xcode.app - renaming back to Xcode.app")
|
||||
do {
|
||||
return try installedXcodePath.rename(to: "Xcode.app")
|
||||
} catch {
|
||||
Logger.appState.error("Unable to create rename Xcode.app back to original")
|
||||
self.error = error
|
||||
// TODO UPDATE MY ERROR STRING
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
|
||||
.mapError { error in
|
||||
return RenameXcodeError.renameFailure(error)
|
||||
}
|
||||
return nil
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) {
|
||||
|
|
@ -931,17 +974,11 @@ class AppState: ObservableObject {
|
|||
// MARK: - Private
|
||||
|
||||
private func uninstallXcode(path: Path) -> AnyPublisher<Void, Error> {
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
do {
|
||||
try Current.files.trashItem(at: path.url)
|
||||
promise(.success(()))
|
||||
} catch {
|
||||
promise(.failure(error))
|
||||
}
|
||||
return installHelperIfNecessary()
|
||||
.flatMap { _ in
|
||||
Current.helper.remove(path.string)
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// removes saved username and credentials stored in keychain
|
||||
|
|
|
|||
|
|
@ -422,6 +422,7 @@ public struct Defaults {
|
|||
}
|
||||
|
||||
private let helperClient = HelperClient()
|
||||
|
||||
public struct Helper {
|
||||
var install: () throws -> Void = helperClient.install
|
||||
var checkIfLatestHelperIsInstalled: () -> AnyPublisher<Bool, Never> = helperClient.checkIfLatestHelperIsInstalled
|
||||
|
|
@ -431,4 +432,8 @@ public struct Helper {
|
|||
var addStaffToDevelopersGroup: () -> AnyPublisher<Void, Error> = helperClient.addStaffToDevelopersGroup
|
||||
var acceptXcodeLicense: (_ absoluteXcodePath: String) -> AnyPublisher<Void, Error> = helperClient.acceptXcodeLicense
|
||||
var runFirstLaunch: (_ absoluteXcodePath: String) -> AnyPublisher<Void, Error> = helperClient.runFirstLaunch
|
||||
var moveApp: (_ absoluteSourcePath: String, _ absoluteDestinationPath: String) -> AnyPublisher<Void, Error> = helperClient.moveApp(at:to:)
|
||||
var createSymbolicLink: (_ absoluteSourcePath: String, _ absoluteDestinationPath: String) -> AnyPublisher<Void, Error> = helperClient.createSymbolicLink(source:destination:)
|
||||
var rename: (_ absoluteSourcePath: String, _ absoluteDestinationPath: String) -> AnyPublisher<Void, Error> = helperClient.rename(source:destination:)
|
||||
var remove: (_ absolutePath: String) -> AnyPublisher<Void, Error> = helperClient.remove(path:)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,8 +302,163 @@ final class HelperClient {
|
|||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var usePrivilegedHelperForFileOperations: Bool {
|
||||
Current.defaults.bool(forKey: PreferenceKey.usePrivilegeHelperForFileOperations.rawValue) ?? false
|
||||
}
|
||||
|
||||
func moveApp(at source:String, to destination: String) -> AnyPublisher<Void, Error> {
|
||||
if !usePrivilegedHelperForFileOperations {
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
FileOperations.moveApp(at: source, to: destination) { error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
}
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let connectionErrorSubject = PassthroughSubject<String, Error>()
|
||||
guard
|
||||
let helper = self.helper(errorSubject: connectionErrorSubject)
|
||||
else {
|
||||
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
|
||||
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
helper.moveApp(at: source, to: destination) { error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
}
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func createSymbolicLink(source: String, destination: String) -> AnyPublisher<Void, Error> {
|
||||
if !usePrivilegedHelperForFileOperations {
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
FileOperations.createSymbolicLink(source: source, destination: destination) { error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
}
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let connectionErrorSubject = PassthroughSubject<String, Error>()
|
||||
|
||||
guard
|
||||
let helper = self.helper(errorSubject: connectionErrorSubject)
|
||||
else {
|
||||
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
|
||||
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
helper.createSymbolicLink(source: source, destination: destination) { error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
}
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func rename(source: String, destination: String) -> AnyPublisher<Void, Error> {
|
||||
if !usePrivilegedHelperForFileOperations {
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
FileOperations.rename(source: source, destination: destination) { error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
}
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let connectionErrorSubject = PassthroughSubject<String, Error>()
|
||||
|
||||
guard
|
||||
let helper = self.helper(errorSubject: connectionErrorSubject)
|
||||
else {
|
||||
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
|
||||
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
helper.rename(source: source, destination: destination) { error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
}
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func remove(path: String) -> AnyPublisher<Void, Error> {
|
||||
if !usePrivilegedHelperForFileOperations {
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
FileOperations.remove(path: path) { error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
}
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let connectionErrorSubject = PassthroughSubject<String, Error>()
|
||||
|
||||
guard
|
||||
let helper = self.helper(errorSubject: connectionErrorSubject)
|
||||
else {
|
||||
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
|
||||
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
helper.remove(path: path) { error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
}
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Install
|
||||
// From https://github.com/securing/SimpleXPCApp/
|
||||
|
||||
|
|
|
|||
|
|
@ -151,6 +151,9 @@ struct AdvancedPreferencePane: View {
|
|||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("UsePrivilegedHelperForFileOperations", isOn: $appState.usePrivilegedHelperForFileOperations)
|
||||
.disabled(PreferenceKey.usePrivilegeHelperForFileOperations.isManaged())
|
||||
}
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
|
|
|
|||
|
|
@ -237,6 +237,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"%@ (%@)" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%1$@ (%2$@)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ %@ %@" : {
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
|
|
@ -22330,6 +22340,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"UsePrivilegedHelperForFileOperations" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Perform file operations using Privileged Helper"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UseUnxipExperiment" : {
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
|
|
|
|||
|
|
@ -157,6 +157,15 @@ class AppStateTests: XCTestCase {
|
|||
// Helper is already installed
|
||||
subject.helperInstallState = .installed
|
||||
|
||||
Current.helper.moveApp = { _,_ in
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let allXcodesRecorder = subject.$allXcodes.record()
|
||||
let installRecorder = subject.install(
|
||||
.version(AvailableXcode(version: Version("0.0.0")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil)),
|
||||
|
|
@ -267,6 +276,15 @@ class AppStateTests: XCTestCase {
|
|||
// Helper is already installed
|
||||
subject.helperInstallState = .installed
|
||||
|
||||
Current.helper.moveApp = { _,_ in
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let allXcodesRecorder = subject.$allXcodes.record()
|
||||
let installRecorder = subject.install(
|
||||
.version(AvailableXcode(version: Version("0.0.0")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil)),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import Foundation
|
|||
import os.log
|
||||
|
||||
class XPCDelegate: NSObject, NSXPCListenerDelegate, HelperXPCProtocol {
|
||||
|
||||
// MARK: - NSXPCListenerDelegate
|
||||
|
||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||
|
|
@ -51,6 +50,26 @@ class XPCDelegate: NSObject, NSXPCListenerDelegate, HelperXPCProtocol {
|
|||
func runFirstLaunch(absoluteXcodePath: String, completion: @escaping (Error?) -> Void) {
|
||||
run(url: URL(fileURLWithPath: absoluteXcodePath + "/Contents/Developer/usr/bin/xcodebuild"), arguments: ["-runFirstLaunch"], completion: completion)
|
||||
}
|
||||
|
||||
func moveApp(at source: String, to destination: String, completion: @escaping ((any Error)?) -> Void) {
|
||||
Logger.xpcDelegate.info("\(#function)")
|
||||
FileOperations.moveApp(at: source, to: destination, completion: completion)
|
||||
}
|
||||
|
||||
func createSymbolicLink(source: String, destination: String, completion: @escaping ((any Error)?) -> Void) {
|
||||
Logger.xpcDelegate.info("\(#function)")
|
||||
FileOperations.createSymbolicLink(source: source, destination: destination, completion: completion)
|
||||
}
|
||||
|
||||
func rename(source: String, destination: String, completion: @escaping ((any Error)?) -> Void) {
|
||||
Logger.xpcDelegate.info("\(#function)")
|
||||
FileOperations.rename(source: source, destination: destination, completion: completion)
|
||||
}
|
||||
|
||||
func remove(path: String, completion: @escaping ((any Error)?) -> Void) {
|
||||
Logger.xpcDelegate.info("\(#function)")
|
||||
FileOperations.remove(path: path, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Run
|
||||
|
|
@ -69,34 +88,3 @@ private func run(url: URL, arguments: [String], completion: @escaping (Error?) -
|
|||
completion(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
struct XPCDelegateError: CustomNSError {
|
||||
enum Code: Int {
|
||||
case invalidXcodePath
|
||||
}
|
||||
|
||||
let code: Code
|
||||
|
||||
init(_ code: Code) {
|
||||
self.code = code
|
||||
}
|
||||
|
||||
// MARK: - CustomNSError
|
||||
|
||||
static var errorDomain: String { "XPCDelegateError" }
|
||||
|
||||
var errorCode: Int { code.rawValue }
|
||||
|
||||
var errorUserInfo: [String : Any] {
|
||||
switch code {
|
||||
case .invalidXcodePath:
|
||||
return [
|
||||
NSLocalizedDescriptionKey: "Invalid Xcode path.",
|
||||
NSLocalizedFailureReasonErrorKey: "Xcode path must be absolute."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue