From 239898f97a0d845c550a0c7e4163f2916b1b1279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Lo=CC=81pez?= Date: Sun, 23 Jan 2022 15:47:50 +0100 Subject: [PATCH] Moved XIP expansion to a temporal directory Instead of expanding XIPs on its current directory, a temporal directory is used. The main advantage of this approach is that, because the temporal directory is created on the same volume where Xcode will be installed, the installation is vastly improved if the XIP is on a different volume. - https://nshipster.com/temporary-files/ - The implementation of the `temporalDirectory` function and variable are based on the similar `trashItem`, but without the `@discardableResult` annotation (I cannot think of any case where this URL could to be ignored). This closes #178. --- Sources/XcodesKit/Environment.swift | 8 +++++++- Sources/XcodesKit/XcodeInstaller.swift | 7 ++++--- Tests/XcodesKitTests/Environment+Mock.swift | 3 ++- Tests/XcodesKitTests/XcodesKitTests.swift | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/XcodesKit/Environment.swift b/Sources/XcodesKit/Environment.swift index fec957d..60c5acb 100644 --- a/Sources/XcodesKit/Environment.swift +++ b/Sources/XcodesKit/Environment.swift @@ -23,7 +23,7 @@ public struct Environment { public var Current = Environment() public struct Shell { - public var unxip: (URL) -> Promise = { Process.run(Path.root.usr.bin.xip, workingDirectory: $0.deletingLastPathComponent(), "--expand", "\($0.path)") } + public var unxip: (URL, URL) -> Promise = { Process.run(Path.root.usr.bin.xip, workingDirectory: $1, "--expand", "\($0.path)") } public var spctlAssess: (URL) -> Promise = { Process.run(Path.root.usr.sbin.spctl, "--assess", "--verbose", "--type", "execute", "\($0.path)") } public var codesignVerify: (URL) -> Promise = { Process.run(Path.root.usr.bin.codesign, "-vv", "-d", "\($0.path)") } public var devToolsSecurityEnable: (String?) -> Promise = { Process.sudo(password: $0, Path.root.usr.sbin.DevToolsSecurity, "-enable") } @@ -238,6 +238,12 @@ public struct Files { try createDirectory(url, createIntermediates, attributes) } + public var temporalDirectory: (URL) throws -> URL = { try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: $0, create: true) } + + public func temporalDirectory(for URL: URL) throws -> URL { + return try temporalDirectory(URL) + } + public var installedXcodes = XcodesKit.installedXcodes } private func installedXcodes(directory: Path) -> [InstalledXcode] { diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index bbd872e..c8ff171 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -718,9 +718,10 @@ public final class XcodeInstaller { } func unarchiveAndMoveXIP(at source: URL, to destination: URL) -> Promise { + let xcodeExpansionDirectory = (try? Current.files.temporalDirectory(for: destination)) ?? source.deletingLastPathComponent() return firstly { () -> Promise in Current.logging.log(InstallationStep.unarchiving.description) - return Current.shell.unxip(source) + return Current.shell.unxip(source, xcodeExpansionDirectory) .recover { (error) throws -> Promise in if case Process.PMKError.execution(_, _, let standardError) = error, standardError?.contains("damaged and can’t be expanded") == true { @@ -732,8 +733,8 @@ public final class XcodeInstaller { .map { output -> URL in Current.logging.log(InstallationStep.moving(destination: destination.path).description) - let xcodeURL = source.deletingLastPathComponent().appendingPathComponent("Xcode.app") - let xcodeBetaURL = source.deletingLastPathComponent().appendingPathComponent("Xcode-beta.app") + let xcodeURL = xcodeExpansionDirectory.appendingPathComponent("Xcode.app") + let xcodeBetaURL = xcodeExpansionDirectory.appendingPathComponent("Xcode-beta.app") if Current.files.fileExists(atPath: xcodeURL.path) { try Current.files.moveItem(at: xcodeURL, to: destination) } diff --git a/Tests/XcodesKitTests/Environment+Mock.swift b/Tests/XcodesKitTests/Environment+Mock.swift index 860cce1..cfd358e 100644 --- a/Tests/XcodesKitTests/Environment+Mock.swift +++ b/Tests/XcodesKitTests/Environment+Mock.swift @@ -16,7 +16,7 @@ extension Shell { static var processOutputMock: ProcessOutput = (0, "", "") static var mock = Shell( - unxip: { _ in return Promise.value(Shell.processOutputMock) }, + unxip: { _, _ in return Promise.value(Shell.processOutputMock) }, spctlAssess: { _ in return Promise.value(Shell.processOutputMock) }, codesignVerify: { _ in return Promise.value(Shell.processOutputMock) }, devToolsSecurityEnable: { _ in return Promise.value(Shell.processOutputMock) }, @@ -60,6 +60,7 @@ extension Files { trashItem: { _ in return URL(fileURLWithPath: "\(NSHomeDirectory())/.Trash") }, createFile: { _, _, _ in return true }, createDirectory: { _, _, _ in }, + temporalDirectory: { _ in return URL(fileURLWithPath: NSTemporaryDirectory()) }, installedXcodes: { _ in [] } ) } diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 947716e..0aafb73 100644 --- a/Tests/XcodesKitTests/XcodesKitTests.swift +++ b/Tests/XcodesKitTests/XcodesKitTests.swift @@ -707,7 +707,7 @@ final class XcodesKitTests: XCTestCase { XcodesKit.Current.logging.log(prompt) return "asdf" } - Current.shell.unxip = { _ in + Current.shell.unxip = { _, _ in unxipCallCount += 1 if unxipCallCount == 1 { return Promise(error: Process.PMKError.execution(process: Process(), standardOutput: nil, standardError: "The file \"Xcode-0.0.0.xip\" is damaged and can’t be expanded."))