mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-04-26 14:57:37 +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 addStaffToDevelopersGroup(completion: @escaping (Error?) -> Void)
|
||||||
func acceptXcodeLicense(absoluteXcodePath: String, completion: @escaping (Error?) -> Void)
|
func acceptXcodeLicense(absoluteXcodePath: String, completion: @escaping (Error?) -> Void)
|
||||||
func runFirstLaunch(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 = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; };
|
||||||
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; };
|
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; };
|
||||||
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
|
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
|
||||||
|
|
@ -197,6 +199,10 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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; };
|
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>"; };
|
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>"; };
|
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -456,6 +462,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */,
|
CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */,
|
||||||
|
150235A02CED5E2200F6ECBF /* FileOperations.swift */,
|
||||||
);
|
);
|
||||||
path = HelperXPCShared;
|
path = HelperXPCShared;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -593,6 +600,9 @@
|
||||||
CAD2E79F2449574E00113D76 /* Products */,
|
CAD2E79F2449574E00113D76 /* Products */,
|
||||||
CA538A12255A4F7C00E64DD7 /* Frameworks */,
|
CA538A12255A4F7C00E64DD7 /* Frameworks */,
|
||||||
CA452BE025A2354D0072DFA4 /* Recovered References */,
|
CA452BE025A2354D0072DFA4 /* Recovered References */,
|
||||||
|
1542A3022CEF05AE00DB71B0 /* App.xcconfig */,
|
||||||
|
1542A3032CEF05B800DB71B0 /* Tests.xcconfig */,
|
||||||
|
1542A3042CEF05C900DB71B0 /* Helper.xcconfig */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|
@ -884,6 +894,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
150235A22CED5E2200F6ECBF /* FileOperations.swift in Sources */,
|
||||||
CA9FF8D025959A9700E47BAF /* HelperXPCShared.swift in Sources */,
|
CA9FF8D025959A9700E47BAF /* HelperXPCShared.swift in Sources */,
|
||||||
CA42DD7325AEB04300BC0B0C /* Logger.swift in Sources */,
|
CA42DD7325AEB04300BC0B0C /* Logger.swift in Sources */,
|
||||||
CA9FF8DB25959B4000E47BAF /* XPCDelegate.swift in Sources */,
|
CA9FF8DB25959B4000E47BAF /* XPCDelegate.swift in Sources */,
|
||||||
|
|
@ -910,6 +921,7 @@
|
||||||
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
||||||
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
||||||
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
||||||
|
150235A12CED5E2200F6ECBF /* FileOperations.swift in Sources */,
|
||||||
CAA8587C25A2B37900ACF8C0 /* IsTesting.swift in Sources */,
|
CAA8587C25A2B37900ACF8C0 /* IsTesting.swift in Sources */,
|
||||||
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
|
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
|
||||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */,
|
CA44901F2463AD34003D8213 /* Tag.swift in Sources */,
|
||||||
|
|
@ -1058,7 +1070,6 @@
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
|
@ -1091,9 +1102,9 @@
|
||||||
};
|
};
|
||||||
CA8FB636256E154800469DA5 /* Test */ = {
|
CA8FB636256E154800469DA5 /* Test */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3022CEF05AE00DB71B0 /* App.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/XcodesTest.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/XcodesTest.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
|
@ -1120,6 +1131,7 @@
|
||||||
};
|
};
|
||||||
CA8FB637256E154800469DA5 /* Test */ = {
|
CA8FB637256E154800469DA5 /* Test */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3032CEF05B800DB71B0 /* Tests.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
|
@ -1144,12 +1156,11 @@
|
||||||
};
|
};
|
||||||
CA9FF8B22595967A00E47BAF /* Debug */ = {
|
CA9FF8B22595967A00E47BAF /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3042CEF05C900DB71B0 /* Helper.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
||||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
|
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
|
||||||
MARKETING_VERSION = 2.0.0;
|
MARKETING_VERSION = 2.0.0;
|
||||||
|
|
@ -1169,9 +1180,9 @@
|
||||||
};
|
};
|
||||||
CA9FF8B32595967A00E47BAF /* Test */ = {
|
CA9FF8B32595967A00E47BAF /* Test */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3042CEF05C900DB71B0 /* Helper.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
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_ENTITLEMENTS = com.xcodesorg.xcodesapp.Helper/com.xcodesorg.xcodesapp.HelperTest.entitlements;
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
||||||
|
|
@ -1196,13 +1207,12 @@
|
||||||
};
|
};
|
||||||
CA9FF8B42595967A00E47BAF /* Release */ = {
|
CA9FF8B42595967A00E47BAF /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3042CEF05C900DB71B0 /* Helper.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
|
||||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
|
INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist";
|
||||||
MARKETING_VERSION = 2.0.0;
|
MARKETING_VERSION = 2.0.0;
|
||||||
|
|
@ -1253,7 +1263,6 @@
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
|
@ -1317,7 +1326,6 @@
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
|
@ -1343,16 +1351,15 @@
|
||||||
};
|
};
|
||||||
CAD2E7BD2449575100113D76 /* Debug */ = {
|
CAD2E7BD2449575100113D76 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3022CEF05AE00DB71B0 /* App.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 30;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = Xcodes/Resources/Info.plist;
|
INFOPLIST_FILE = Xcodes/Resources/Info.plist;
|
||||||
|
|
@ -1371,16 +1378,15 @@
|
||||||
};
|
};
|
||||||
CAD2E7BE2449575100113D76 /* Release */ = {
|
CAD2E7BE2449575100113D76 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3022CEF05AE00DB71B0 /* App.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 30;
|
CURRENT_PROJECT_VERSION = 30;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = Xcodes/Resources/Info.plist;
|
INFOPLIST_FILE = Xcodes/Resources/Info.plist;
|
||||||
|
|
@ -1399,12 +1405,12 @@
|
||||||
};
|
};
|
||||||
CAD2E7C02449575100113D76 /* Debug */ = {
|
CAD2E7C02449575100113D76 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3032CEF05B800DB71B0 /* Tests.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
|
||||||
INFOPLIST_FILE = XcodesTests/Info.plist;
|
INFOPLIST_FILE = XcodesTests/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|
@ -1421,13 +1427,13 @@
|
||||||
};
|
};
|
||||||
CAD2E7C12449575100113D76 /* Release */ = {
|
CAD2E7C12449575100113D76 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1542A3032CEF05B800DB71B0 /* Tests.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
|
||||||
INFOPLIST_FILE = XcodesTests/Info.plist;
|
INFOPLIST_FILE = XcodesTests/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|
|
||||||
|
|
@ -266,18 +266,27 @@ extension AppState {
|
||||||
return Fail(error: error)
|
return Fail(error: error)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
.tryMap { output -> URL in
|
.flatMap { output -> AnyPublisher<URL, Swift.Error> in
|
||||||
self.setInstallationStep(of: availableXcode.version, to: .moving(destination: destination.path))
|
self.setInstallationStep(of: availableXcode.version, to: .moving(destination: destination.path))
|
||||||
|
|
||||||
let xcodeURL = source.deletingLastPathComponent().appendingPathComponent("Xcode.app")
|
let xcodeURL = source.deletingLastPathComponent().appendingPathComponent("Xcode.app")
|
||||||
let xcodeBetaURL = source.deletingLastPathComponent().appendingPathComponent("Xcode-beta.app")
|
let xcodeBetaURL = source.deletingLastPathComponent().appendingPathComponent("Xcode-beta.app")
|
||||||
if Current.files.fileExists(atPath: xcodeURL.path) {
|
if Current.files.fileExists(atPath: xcodeURL.path) {
|
||||||
try Current.files.moveItem(at: xcodeURL, to: destination)
|
return Current.helper.moveApp(xcodeURL.path, destination.path)
|
||||||
}
|
.map { _ in destination }
|
||||||
else if Current.files.fileExists(atPath: xcodeBetaURL.path) {
|
.eraseToAnyPublisher()
|
||||||
try Current.files.moveItem(at: xcodeBetaURL, to: destination)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return destination
|
||||||
}
|
}
|
||||||
.handleEvents(receiveCancel: {
|
.handleEvents(receiveCancel: {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ enum PreferenceKey: String {
|
||||||
case xcodeListCategory
|
case xcodeListCategory
|
||||||
case allowedMajorVersions
|
case allowedMajorVersions
|
||||||
case hideSupportXcodes
|
case hideSupportXcodes
|
||||||
|
case usePrivilegeHelperForFileOperations
|
||||||
|
|
||||||
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
|
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +70,14 @@ class AppState: ObservableObject {
|
||||||
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
|
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
|
||||||
@Published var presentedAlert: XcodesAlert?
|
@Published var presentedAlert: XcodesAlert?
|
||||||
@Published var presentedPreferenceAlert: XcodesPreferencesAlert?
|
@Published var presentedPreferenceAlert: XcodesPreferencesAlert?
|
||||||
|
|
||||||
@Published var helperInstallState: HelperInstallState = .notInstalled
|
@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.
|
/// 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.
|
/// This closure will be performed after the user chooses whether or not to proceed.
|
||||||
@Published var isPreparingUserForActionRequiringHelper: ((Bool) -> Void)?
|
@Published var isPreparingUserForActionRequiringHelper: ((Bool) -> Void)?
|
||||||
|
|
@ -150,6 +158,7 @@ class AppState: ObservableObject {
|
||||||
internal var runtimePublishers: [String: Task<(), any Error>] = [:]
|
internal var runtimePublishers: [String: Task<(), any Error>] = [:]
|
||||||
private var selectPublisher: AnyCancellable?
|
private var selectPublisher: AnyCancellable?
|
||||||
private var uninstallPublisher: AnyCancellable?
|
private var uninstallPublisher: AnyCancellable?
|
||||||
|
private var createSymLinkPublisher: AnyCancellable?
|
||||||
private var autoInstallTimer: Timer?
|
private var autoInstallTimer: Timer?
|
||||||
|
|
||||||
// MARK: - Dock Progress Tracking
|
// MARK: - Dock Progress Tracking
|
||||||
|
|
@ -702,23 +711,26 @@ class AppState: ObservableObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard
|
guard selectPublisher == nil else { return }
|
||||||
var installedXcodePath = xcode.installedPath,
|
|
||||||
selectPublisher == nil
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
if onSelectActionType == .rename {
|
|
||||||
guard let newDestinationXcodePath = renameToXcode(xcode: xcode) else { return }
|
|
||||||
installedXcodePath = newDestinationXcodePath
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPublisher = installHelperIfNecessary()
|
selectPublisher = installHelperIfNecessary()
|
||||||
.flatMap {
|
.flatMap { [unowned self] _ -> AnyPublisher<String, Error> in
|
||||||
Current.helper.switchXcodePath(installedXcodePath.string)
|
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
|
.flatMap { [unowned self] _ in
|
||||||
self.updateSelectedXcodePath()
|
self.updateSelectedXcodePath()
|
||||||
}
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(
|
.sink(
|
||||||
receiveCompletion: { [unowned self] completion in
|
receiveCompletion: { [unowned self] completion in
|
||||||
if case let .failure(error) = completion {
|
if case let .failure(error) = completion {
|
||||||
|
|
@ -769,66 +781,97 @@ class AppState: ObservableObject {
|
||||||
guard let installedXcodePath = xcode.installedPath else { return }
|
guard let installedXcodePath = xcode.installedPath else { return }
|
||||||
|
|
||||||
let destinationPath: Path = Path.installDirectory/"Xcode\(isBeta ? "-Beta" : "").app"
|
let destinationPath: Path = Path.installDirectory/"Xcode\(isBeta ? "-Beta" : "").app"
|
||||||
|
|
||||||
// does an Xcode.app file exist?
|
createSymLinkPublisher = installHelperIfNecessary()
|
||||||
if FileManager.default.fileExists(atPath: destinationPath.string) {
|
.flatMap {
|
||||||
do {
|
Current.helper.createSymbolicLink(installedXcodePath.string, destinationPath.string)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
.sink(
|
||||||
|
receiveCompletion: { [unowned self] completion in
|
||||||
do {
|
if case let .failure(error) = completion {
|
||||||
try FileManager.default.createSymbolicLink(atPath: destinationPath.string, withDestinationPath: installedXcodePath.string)
|
if let error = error as? CustomNSError {
|
||||||
Logger.appState.info("Successfully created symbolic link with Xcode\(isBeta ? "-Beta": "").app")
|
switch error.errorCode {
|
||||||
} catch {
|
case XPCDelegateError.Code.destinationIsNotASymbolicLink.rawValue:
|
||||||
Logger.appState.error("Unable to create symbolic Link")
|
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: localizeString("Alert.SymLink.Message"))
|
||||||
self.error = error
|
default:
|
||||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
|
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? {
|
enum RenameXcodeError: Error {
|
||||||
guard let installedXcodePath = xcode.installedPath else { return nil }
|
case xcodeInstallPathIsNil
|
||||||
|
case xcodeAppNotFound
|
||||||
let destinationPath: Path = Path.installDirectory/"Xcode.app"
|
case originalXcodeNameNotFound
|
||||||
|
case renameFailure(Error)
|
||||||
// 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) {
|
func renameToXcode(xcode: Xcode) -> AnyPublisher<String, Error> {
|
||||||
let newName = "Xcode-\(originalXcode.version.descriptionWithoutBuildMetadata).app"
|
guard let installedXcodePath = xcode.installedPath else {
|
||||||
Logger.appState.debug("Found Xcode.app - renaming back to \(newName)")
|
return Fail(error: RenameXcodeError.xcodeInstallPathIsNil)
|
||||||
do {
|
.eraseToAnyPublisher()
|
||||||
try destinationPath.rename(to: newName)
|
}
|
||||||
} catch {
|
|
||||||
Logger.appState.error("Unable to create rename Xcode.app back to original")
|
var cancellables: Set<AnyCancellable> = []
|
||||||
self.error = error
|
|
||||||
// TODO UPDATE MY ERROR STRING
|
return Future<String, Error> { promise in
|
||||||
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
|
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
|
.mapError { error in
|
||||||
Logger.appState.debug("Found Xcode.app - renaming back to Xcode.app")
|
return RenameXcodeError.renameFailure(error)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
return nil
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) {
|
func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) {
|
||||||
|
|
@ -931,17 +974,11 @@ class AppState: ObservableObject {
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func uninstallXcode(path: Path) -> AnyPublisher<Void, Error> {
|
private func uninstallXcode(path: Path) -> AnyPublisher<Void, Error> {
|
||||||
return Deferred {
|
return installHelperIfNecessary()
|
||||||
Future { promise in
|
.flatMap { _ in
|
||||||
do {
|
Current.helper.remove(path.string)
|
||||||
try Current.files.trashItem(at: path.url)
|
|
||||||
promise(.success(()))
|
|
||||||
} catch {
|
|
||||||
promise(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
.eraseToAnyPublisher()
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// removes saved username and credentials stored in keychain
|
/// removes saved username and credentials stored in keychain
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,7 @@ public struct Defaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
private let helperClient = HelperClient()
|
private let helperClient = HelperClient()
|
||||||
|
|
||||||
public struct Helper {
|
public struct Helper {
|
||||||
var install: () throws -> Void = helperClient.install
|
var install: () throws -> Void = helperClient.install
|
||||||
var checkIfLatestHelperIsInstalled: () -> AnyPublisher<Bool, Never> = helperClient.checkIfLatestHelperIsInstalled
|
var checkIfLatestHelperIsInstalled: () -> AnyPublisher<Bool, Never> = helperClient.checkIfLatestHelperIsInstalled
|
||||||
|
|
@ -431,4 +432,8 @@ public struct Helper {
|
||||||
var addStaffToDevelopersGroup: () -> AnyPublisher<Void, Error> = helperClient.addStaffToDevelopersGroup
|
var addStaffToDevelopersGroup: () -> AnyPublisher<Void, Error> = helperClient.addStaffToDevelopersGroup
|
||||||
var acceptXcodeLicense: (_ absoluteXcodePath: String) -> AnyPublisher<Void, Error> = helperClient.acceptXcodeLicense
|
var acceptXcodeLicense: (_ absoluteXcodePath: String) -> AnyPublisher<Void, Error> = helperClient.acceptXcodeLicense
|
||||||
var runFirstLaunch: (_ absoluteXcodePath: String) -> AnyPublisher<Void, Error> = helperClient.runFirstLaunch
|
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()
|
.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
|
// MARK: - Install
|
||||||
// From https://github.com/securing/SimpleXPCApp/
|
// From https://github.com/securing/SimpleXPCApp/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,9 @@ struct AdvancedPreferencePane: View {
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
Toggle("UsePrivilegedHelperForFileOperations", isOn: $appState.usePrivilegedHelperForFileOperations)
|
||||||
|
.disabled(PreferenceKey.usePrivilegeHelperForFileOperations.isManaged())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"%@ (%@)" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@ (%2$@)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"%@ %@ %@" : {
|
"%@ %@ %@" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -22330,6 +22340,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"UsePrivilegedHelperForFileOperations" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Perform file operations using Privileged Helper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"UseUnxipExperiment" : {
|
"UseUnxipExperiment" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,15 @@ class AppStateTests: XCTestCase {
|
||||||
// Helper is already installed
|
// Helper is already installed
|
||||||
subject.helperInstallState = .installed
|
subject.helperInstallState = .installed
|
||||||
|
|
||||||
|
Current.helper.moveApp = { _,_ in
|
||||||
|
return Deferred {
|
||||||
|
Future { promise in
|
||||||
|
promise(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
let allXcodesRecorder = subject.$allXcodes.record()
|
let allXcodesRecorder = subject.$allXcodes.record()
|
||||||
let installRecorder = subject.install(
|
let installRecorder = subject.install(
|
||||||
.version(AvailableXcode(version: Version("0.0.0")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil)),
|
.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
|
// Helper is already installed
|
||||||
subject.helperInstallState = .installed
|
subject.helperInstallState = .installed
|
||||||
|
|
||||||
|
Current.helper.moveApp = { _,_ in
|
||||||
|
return Deferred {
|
||||||
|
Future { promise in
|
||||||
|
promise(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
let allXcodesRecorder = subject.$allXcodes.record()
|
let allXcodesRecorder = subject.$allXcodes.record()
|
||||||
let installRecorder = subject.install(
|
let installRecorder = subject.install(
|
||||||
.version(AvailableXcode(version: Version("0.0.0")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil)),
|
.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
|
import os.log
|
||||||
|
|
||||||
class XPCDelegate: NSObject, NSXPCListenerDelegate, HelperXPCProtocol {
|
class XPCDelegate: NSObject, NSXPCListenerDelegate, HelperXPCProtocol {
|
||||||
|
|
||||||
// MARK: - NSXPCListenerDelegate
|
// MARK: - NSXPCListenerDelegate
|
||||||
|
|
||||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
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) {
|
func runFirstLaunch(absoluteXcodePath: String, completion: @escaping (Error?) -> Void) {
|
||||||
run(url: URL(fileURLWithPath: absoluteXcodePath + "/Contents/Developer/usr/bin/xcodebuild"), arguments: ["-runFirstLaunch"], completion: completion)
|
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
|
// MARK: - Run
|
||||||
|
|
@ -69,34 +88,3 @@ private func run(url: URL, arguments: [String], completion: @escaping (Error?) -
|
||||||
completion(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