From 6fe3a3e5cf8bcfcccef7b2a8ec49430777d4ab0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Lo=CC=81pez?= Date: Sat, 5 Feb 2022 13:00:50 +0100 Subject: [PATCH] Added `expand-xip-inplace` flag to prevent expanding on a temporal directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As suggested by @MattKiazyk, to allow the user to revert to the old behaviour if needed. - For testing this flag is set to `true`, to use a well-known location when testing. - The `xcodeExpansionDirectory` function cannot be easily expressed on a single line anymore, so removed the variable added on the previous commit (which wasn’t used anyway). --- Sources/XcodesKit/Environment.swift | 9 +++++---- Sources/XcodesKit/XcodeInstaller.swift | 18 +++++++++--------- Sources/xcodes/main.swift | 5 ++++- Tests/XcodesKitTests/Environment+Mock.swift | 1 - Tests/XcodesKitTests/XcodesKitTests.swift | 20 ++++++++++---------- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Sources/XcodesKit/Environment.swift b/Sources/XcodesKit/Environment.swift index cb8f3e9..fc72581 100644 --- a/Sources/XcodesKit/Environment.swift +++ b/Sources/XcodesKit/Environment.swift @@ -244,10 +244,11 @@ public struct Files { return try temporalDirectory(URL) } - public var xcodeExpansionDirectory: (URL, URL) -> URL = { (try? Current.files.temporalDirectory(for: $1)) ?? $0.deletingLastPathComponent() } - - public func xcodeExpansionDirectory(archiveURL: URL, xcodeURL: URL) -> URL { - return xcodeExpansionDirectory(archiveURL, xcodeURL) + public func xcodeExpansionDirectory(archiveURL: URL, xcodeURL: URL, shouldExpandInplace: Bool) -> URL { + if shouldExpandInplace { + return archiveURL.deletingLastPathComponent() + } + return (try? Current.files.temporalDirectory(for: xcodeURL)) ?? archiveURL.deletingLastPathComponent() } public var installedXcodes = XcodesKit.installedXcodes diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index 77ad5d7..6de9403 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -155,9 +155,9 @@ public final class XcodeInstaller { case aria2(Path) } - public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path) -> Promise { + public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, shouldExpandXipInplace: Bool) -> Promise { return firstly { () -> Promise in - return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0) + return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, shouldExpandXipInplace: shouldExpandXipInplace, attemptNumber: 0) } .done { xcode in Current.logging.log("\nXcode \(xcode.version.descriptionWithoutBuildMetadata) has been installed to \(xcode.path.string)".green) @@ -165,12 +165,12 @@ public final class XcodeInstaller { } } - private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int) -> Promise { + private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, shouldExpandXipInplace: Bool, attemptNumber: Int) -> Promise { return firstly { () -> Promise<(Xcode, URL)> in return self.getXcodeArchive(installationType, dataSource: dataSource, downloader: downloader, destination: destination, willInstall: true) } .then { xcode, url -> Promise in - return self.installArchivedXcode(xcode, at: url, to: destination) + return self.installArchivedXcode(xcode, at: url, to: destination, shouldExpandXipInplace: shouldExpandXipInplace) } .recover { error -> Promise in switch error { @@ -187,7 +187,7 @@ public final class XcodeInstaller { Current.logging.log(error.legibleLocalizedDescription.red) Current.logging.log("Removing damaged XIP and re-attempting installation.\n") try Current.files.removeItem(at: damagedXIPURL) - return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: attemptNumber + 1) + return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, shouldExpandXipInplace: shouldExpandXipInplace, attemptNumber: attemptNumber + 1) } } default: @@ -520,7 +520,7 @@ public final class XcodeInstaller { } } - public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path) -> Promise { + public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, shouldExpandXipInplace: Bool) -> Promise { let passwordInput = { Promise { seal in Current.logging.log("xcodes requires superuser privileges in order to finish installation.") @@ -533,7 +533,7 @@ public final class XcodeInstaller { let destinationURL = destination.join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url switch archiveURL.pathExtension { case "xip": - return unarchiveAndMoveXIP(at: archiveURL, to: destinationURL).map { xcodeURL in + return unarchiveAndMoveXIP(at: archiveURL, to: destinationURL, shouldExpandXipInplace: shouldExpandXipInplace).map { xcodeURL in guard let path = Path(url: xcodeURL), Current.files.fileExists(atPath: path.string), @@ -717,8 +717,8 @@ public final class XcodeInstaller { } } - func unarchiveAndMoveXIP(at source: URL, to destination: URL) -> Promise { - let xcodeExpansionDirectory = Current.files.xcodeExpansionDirectory(archiveURL: source, xcodeURL: destination) + func unarchiveAndMoveXIP(at source: URL, to destination: URL, shouldExpandXipInplace: Bool) -> Promise { + let xcodeExpansionDirectory = Current.files.xcodeExpansionDirectory(archiveURL: source, xcodeURL: destination, shouldExpandInplace: shouldExpandXipInplace) return firstly { () -> Promise in Current.logging.log(InstallationStep.unarchiving.description) return Current.shell.unxip(source, xcodeExpansionDirectory) diff --git a/Sources/xcodes/main.swift b/Sources/xcodes/main.swift index 5e37158..825edee 100644 --- a/Sources/xcodes/main.swift +++ b/Sources/xcodes/main.swift @@ -187,6 +187,9 @@ struct Xcodes: ParsableCommand { completion: .directory) var directory: String? + @Flag(help: "Expands (decompress) Xcode .xip on the same directory it's downloaded, instead of using a temporal directory.") + var expandXipInplace: Bool = false + @OptionGroup var globalDataSource: GlobalDataSourceOption @@ -218,7 +221,7 @@ struct Xcodes: ParsableCommand { let destination = getDirectory(possibleDirectory: directory) - installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination) + installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, shouldExpandXipInplace: expandXipInplace) .done { Install.exit() } .catch { error in Install.processDownloadOrInstall(error: error) diff --git a/Tests/XcodesKitTests/Environment+Mock.swift b/Tests/XcodesKitTests/Environment+Mock.swift index 92b1039..cfd358e 100644 --- a/Tests/XcodesKitTests/Environment+Mock.swift +++ b/Tests/XcodesKitTests/Environment+Mock.swift @@ -61,7 +61,6 @@ extension Files { createFile: { _, _, _ in return true }, createDirectory: { _, _, _ in }, temporalDirectory: { _ in return URL(fileURLWithPath: NSTemporaryDirectory()) }, - xcodeExpansionDirectory: { _, _ in return URL(fileURLWithPath: NSTemporaryDirectory()) }, installedXcodes: { _ in [] } ) } diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 0aafb73..2ba5993 100644 --- a/Tests/XcodesKitTests/XcodesKitTests.swift +++ b/Tests/XcodesKitTests/XcodesKitTests.swift @@ -86,7 +86,7 @@ final class XcodesKitTests: XCTestCase { let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil) let installedXcode = InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)! - installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications")) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), shouldExpandXipInplace: true) .catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.failedSecurityAssessment(xcode: installedXcode, output: "")) } } @@ -94,7 +94,7 @@ final class XcodesKitTests: XCTestCase { Current.shell.codesignVerify = { _ in return Promise(error: Process.PMKError.execution(process: Process(), standardOutput: nil, standardError: nil)) } let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil) - installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications")) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), shouldExpandXipInplace: true) .catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.codesignVerifyFailed(output: "")) } } @@ -102,7 +102,7 @@ final class XcodesKitTests: XCTestCase { Current.shell.codesignVerify = { _ in return Promise.value((0, "", "")) } let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil) - installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications")) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), shouldExpandXipInplace: true) .catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.unexpectedCodeSigningIdentity(identifier: "", certificateAuthority: [])) } } @@ -115,7 +115,7 @@ final class XcodesKitTests: XCTestCase { let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil) let xipURL = URL(fileURLWithPath: "/Xcode-0.0.0.xip") - installer.installArchivedXcode(xcode, at: xipURL, to: Path.root.join("Applications")) + installer.installArchivedXcode(xcode, at: xipURL, to: Path.root.join("Applications"), shouldExpandXipInplace: true) .ensure { XCTAssertEqual(trashedItemAtURL, xipURL) } .cauterize() } @@ -203,7 +203,7 @@ final class XcodesKitTests: XCTestCase { let expectation = self.expectation(description: "Finished") - installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications")) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), shouldExpandXipInplace: true) .ensure { let url = Bundle.module.url(forResource: "LogOutput-FullHappyPath", withExtension: "txt", subdirectory: "Fixtures")! XCTAssertEqual(log, try! String(contentsOf: url)) @@ -296,7 +296,7 @@ final class XcodesKitTests: XCTestCase { let expectation = self.expectation(description: "Finished") - installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications")) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), shouldExpandXipInplace: true) .ensure { let url = Bundle.module.url(forResource: "LogOutput-FullHappyPath-NoColor", withExtension: "txt", subdirectory: "Fixtures")! XCTAssertEqual(log, try! String(contentsOf: url)) @@ -393,7 +393,7 @@ final class XcodesKitTests: XCTestCase { let expectation = self.expectation(description: "Finished") - installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications")) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), shouldExpandXipInplace: true) .ensure { let url = Bundle.module.url(forResource: "LogOutput-FullHappyPath-NonInteractiveTerminal", withExtension: "txt", subdirectory: "Fixtures")! XCTAssertEqual(log, try! String(contentsOf: url)) @@ -486,7 +486,7 @@ final class XcodesKitTests: XCTestCase { let expectation = self.expectation(description: "Finished") - installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.home.join("Xcode")) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.home.join("Xcode"), shouldExpandXipInplace: true) .ensure { let url = Bundle.module.url(forResource: "LogOutput-AlternativeDirectory", withExtension: "txt", subdirectory: "Fixtures")! let expectedText = try! String(contentsOf: url).replacingOccurrences(of: "/Users/brandon", with: Path.home.string) @@ -600,7 +600,7 @@ final class XcodesKitTests: XCTestCase { let expectation = self.expectation(description: "Finished") - installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications")) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), shouldExpandXipInplace: true) .ensure { let url = Bundle.module.url(forResource: "LogOutput-IncorrectSavedPassword", withExtension: "txt", subdirectory: "Fixtures")! XCTAssertEqual(log, try! String(contentsOf: url)) @@ -718,7 +718,7 @@ final class XcodesKitTests: XCTestCase { let expectation = self.expectation(description: "Finished") - installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications")) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), shouldExpandXipInplace: true) .ensure { let url = Bundle.module.url(forResource: "LogOutput-DamagedXIP", withExtension: "txt", subdirectory: "Fixtures")! let expectedText = try! String(contentsOf: url).replacingOccurrences(of: "/Users/brandon", with: Path.home.string)