Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions HelperXPCShared/FileOperations.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation
import os.log

enum FileOperations {
private static let 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)
}
}
}
50 changes: 50 additions & 0 deletions HelperXPCShared/HelperXPCShared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,54 @@ protocol HelperXPCProtocol: Sendable {
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."
]
}
}
}
12 changes: 9 additions & 3 deletions Xcodes.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
1596C2913043765600178C86 /* FileOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1596C2903043765600178C86 /* FileOperations.swift */; };
1596C2923043765600178C86 /* FileOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1596C2903043765600178C86 /* FileOperations.swift */; };
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; };
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */; };
Expand Down Expand Up @@ -184,6 +186,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
1596C2903043765600178C86 /* FileOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperations.swift; 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>"; };
Expand Down Expand Up @@ -430,6 +433,7 @@
isa = PBXGroup;
children = (
CA9FF8CE25959A9700E47BAF /* HelperXPCShared.swift */,
1596C2903043765600178C86 /* FileOperations.swift */,
);
path = HelperXPCShared;
sourceTree = "<group>";
Expand Down Expand Up @@ -860,6 +864,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1596C2913043765600178C86 /* FileOperations.swift in Sources */,
CA9FF8D025959A9700E47BAF /* HelperXPCShared.swift in Sources */,
CA42DD7325AEB04300BC0B0C /* Logger.swift in Sources */,
CA9FF8DB25959B4000E47BAF /* XPCDelegate.swift in Sources */,
Expand Down Expand Up @@ -908,6 +913,7 @@
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */,
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
1596C2923043765600178C86 /* FileOperations.swift in Sources */,
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
B0403CF02AD92D7B00137C09 /* ReleaseNotesView.swift in Sources */,
CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */,
Expand Down Expand Up @@ -1514,10 +1520,10 @@
};
E899297E2FFDFA9A0019DB31 /* XCRemoteSwiftPackageReference "XcodesKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/XcodesOrg/XcodesKit";
repositoryURL = "https://github.com/abiligiri/XcodesKit";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 1.0.4;
branch = "async-move-item-for-helper";
kind = branch;
};
};
E89CBD3B2D5FC0B10037ED95 /* XCRemoteSwiftPackageReference "XcodesLoginKit" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion Xcodes/Backend/AppState+Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,14 @@ extension AppState {
XcodeUnarchiveService(
unarchive: { _ = try await self.unxipOrUnxipExperimentAsync($0) },
fileExists: { path in Current.files.fileExists(atPath: path) },
moveItem: { source, destination in try Current.files.moveItem(at: source, to: destination) },
moveItem: { source, destination in
if Current.helper.usePrivilegedHelperForFileOperations {
try await self.installHelperIfNecessaryAsync()
try await Current.helper.moveAppAsync(source.path, destination.path)
} else {
try Current.files.moveItem(at: source, to: destination)
}
},
removeItem: { url in try Current.files.removeItem(at: url) }
)
}
Expand Down
Loading
Loading