From d6690d27c64581d0c55ed6c2ecbd27d323b07b31 Mon Sep 17 00:00:00 2001 From: Mahmood Tahir Date: Tue, 15 Mar 2022 23:04:41 -0400 Subject: [PATCH 01/10] Fix broken unit tests due to new logs --- .../Fixtures/LogOutput-AlternativeDirectory.txt | 3 +++ Tests/XcodesKitTests/Fixtures/LogOutput-DamagedXIP.txt | 4 ++++ .../Fixtures/LogOutput-FullHappyPath-NoColor.txt | 3 +++ .../LogOutput-FullHappyPath-NonInteractiveTerminal.txt | 1 + Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath.txt | 3 +++ .../Fixtures/LogOutput-IncorrectSavedPassword.txt | 3 +++ 6 files changed, 17 insertions(+) diff --git a/Tests/XcodesKitTests/Fixtures/LogOutput-AlternativeDirectory.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-AlternativeDirectory.txt index 1a462e0..7e0d0f7 100644 --- a/Tests/XcodesKitTests/Fixtures/LogOutput-AlternativeDirectory.txt +++ b/Tests/XcodesKitTests/Fixtures/LogOutput-AlternativeDirectory.txt @@ -1,6 +1,8 @@ Apple ID: Apple ID Password: +Downloading with urlSession - for faster downloads install aria2 (`brew install aria2`) + (1/6) Downloading Xcode 0.0.0: 1% (1/6) Downloading Xcode 0.0.0: 2% (1/6) Downloading Xcode 0.0.0: 3% @@ -102,6 +104,7 @@ Apple ID Password: (1/6) Downloading Xcode 0.0.0: 99% (1/6) Downloading Xcode 0.0.0: 100% (2/6) Unarchiving Xcode (This can take a while) +Using regular unxip. Try passing `--experimental-unxip` for a faster unxip process (3/6) Moving Xcode to /Users/brandon/Xcode/Xcode-0.0.0.app (4/6) Moving Xcode archive Xcode-0.0.0.xip to the Trash (5/6) Checking security assessment and code signing diff --git a/Tests/XcodesKitTests/Fixtures/LogOutput-DamagedXIP.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-DamagedXIP.txt index e7f7d0b..c3a0e12 100644 --- a/Tests/XcodesKitTests/Fixtures/LogOutput-DamagedXIP.txt +++ b/Tests/XcodesKitTests/Fixtures/LogOutput-DamagedXIP.txt @@ -3,10 +3,13 @@ Apple ID Password: (1/6) Found existing archive that will be used for installation at /Users/brandon/Library/Application Support/com.robotsandpencils.xcodes/Xcode-0.0.0.xip. (2/6) Unarchiving Xcode (This can take a while) +Using regular unxip. Try passing `--experimental-unxip` for a faster unxip process The archive "Xcode-0.0.0.xip" is damaged and can't be expanded. Removing damaged XIP and re-attempting installation. +Downloading with urlSession - for faster downloads install aria2 (`brew install aria2`) + (1/6) Downloading Xcode 0.0.0: 1% (1/6) Downloading Xcode 0.0.0: 2% (1/6) Downloading Xcode 0.0.0: 3% @@ -108,6 +111,7 @@ Removing damaged XIP and re-attempting installation. (1/6) Downloading Xcode 0.0.0: 99% (1/6) Downloading Xcode 0.0.0: 100% (2/6) Unarchiving Xcode (This can take a while) +Using regular unxip. Try passing `--experimental-unxip` for a faster unxip process (3/6) Moving Xcode to /Applications/Xcode-0.0.0.app (4/6) Moving Xcode archive Xcode-0.0.0.xip to the Trash (5/6) Checking security assessment and code signing diff --git a/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath-NoColor.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath-NoColor.txt index f562fab..c2c1d68 100644 --- a/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath-NoColor.txt +++ b/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath-NoColor.txt @@ -1,6 +1,8 @@ Apple ID: Apple ID Password: +Downloading with urlSession - for faster downloads install aria2 (`brew install aria2`) + (1/6) Downloading Xcode 0.0.0: 1% (1/6) Downloading Xcode 0.0.0: 2% (1/6) Downloading Xcode 0.0.0: 3% @@ -102,6 +104,7 @@ Apple ID Password: (1/6) Downloading Xcode 0.0.0: 99% (1/6) Downloading Xcode 0.0.0: 100% (2/6) Unarchiving Xcode (This can take a while) +Using regular unxip. Try passing `--experimental-unxip` for a faster unxip process (3/6) Moving Xcode to /Applications/Xcode-0.0.0.app (4/6) Moving Xcode archive Xcode-0.0.0.xip to the Trash (5/6) Checking security assessment and code signing diff --git a/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath-NonInteractiveTerminal.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath-NonInteractiveTerminal.txt index eaa8932..e43b59a 100644 --- a/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath-NonInteractiveTerminal.txt +++ b/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath-NonInteractiveTerminal.txt @@ -2,6 +2,7 @@ Apple ID: Apple ID Password: (1/6) Downloading Xcode 0.0.0 (2/6) Unarchiving Xcode (This can take a while) +Using regular unxip. Try passing `--experimental-unxip` for a faster unxip process (3/6) Moving Xcode to /Applications/Xcode-0.0.0.app (4/6) Moving Xcode archive Xcode-0.0.0.xip to the Trash (5/6) Checking security assessment and code signing diff --git a/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath.txt index 579622b..190aca5 100644 --- a/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath.txt +++ b/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath.txt @@ -1,6 +1,8 @@ Apple ID: Apple ID Password: +Downloading with urlSession - for faster downloads install aria2 (`brew install aria2`) + (1/6) Downloading Xcode 0.0.0: 1% (1/6) Downloading Xcode 0.0.0: 2% (1/6) Downloading Xcode 0.0.0: 3% @@ -102,6 +104,7 @@ Apple ID Password: (1/6) Downloading Xcode 0.0.0: 99% (1/6) Downloading Xcode 0.0.0: 100% (2/6) Unarchiving Xcode (This can take a while) +Using regular unxip. Try passing `--experimental-unxip` for a faster unxip process (3/6) Moving Xcode to /Applications/Xcode-0.0.0.app (4/6) Moving Xcode archive Xcode-0.0.0.xip to the Trash (5/6) Checking security assessment and code signing diff --git a/Tests/XcodesKitTests/Fixtures/LogOutput-IncorrectSavedPassword.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-IncorrectSavedPassword.txt index 7f8aff9..b3b59f4 100644 --- a/Tests/XcodesKitTests/Fixtures/LogOutput-IncorrectSavedPassword.txt +++ b/Tests/XcodesKitTests/Fixtures/LogOutput-IncorrectSavedPassword.txt @@ -3,6 +3,8 @@ Invalid username and password combination. Attempted to sign in with username te Try entering your password again Apple ID Password (test@example.com): +Downloading with urlSession - for faster downloads install aria2 (`brew install aria2`) + (1/6) Downloading Xcode 0.0.0: 1% (1/6) Downloading Xcode 0.0.0: 2% (1/6) Downloading Xcode 0.0.0: 3% @@ -104,6 +106,7 @@ Apple ID Password (test@example.com): (1/6) Downloading Xcode 0.0.0: 99% (1/6) Downloading Xcode 0.0.0: 100% (2/6) Unarchiving Xcode (This can take a while) +Using regular unxip. Try passing `--experimental-unxip` for a faster unxip process (3/6) Moving Xcode to /Applications/Xcode-0.0.0.app (4/6) Moving Xcode archive Xcode-0.0.0.xip to the Trash (5/6) Checking security assessment and code signing From 40736264ae4fba348b7949ce532546da853dec88 Mon Sep 17 00:00:00 2001 From: Mahmood Tahir Date: Tue, 15 Mar 2022 23:11:02 -0400 Subject: [PATCH 02/10] Use xcode 13 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65a669c..2bfdb38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,10 @@ name: CI on: [push, pull_request] jobs: test: - runs-on: macOS-10.15 + runs-on: macOS-11 steps: - uses: actions/checkout@v3 - name: Run tests env: - DEVELOPER_DIR: /Applications/Xcode_12.3.app + DEVELOPER_DIR: /Applications/Xcode_13.2.1.app run: swift test From d3418fa0541b813d5a347f2f1fad8bbf746474d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Lo=CC=81pez?= Date: Sun, 6 Feb 2022 13:56:34 +0100 Subject: [PATCH 03/10] Encapsulated actions that require superuser permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is needed for the next commits and I think it makes the code more organised (it’s clear why a `passwordInput` is being requested, because all actions use it). --- Sources/XcodesKit/XcodeInstaller.swift | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index 81aba81..069dcdc 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -529,14 +529,6 @@ public final class XcodeInstaller { } public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false) -> Promise { - let passwordInput = { - Promise { seal in - Current.logging.log("xcodes requires superuser privileges in order to finish installation.") - guard let password = Current.shell.readSecureLine(prompt: "macOS User Password: ") else { seal.reject(Error.missingSudoerPassword); return } - seal.fulfill(password + "\n") - } - } - return firstly { () -> Promise in let destinationURL = destination.join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url switch archiveURL.pathExtension { @@ -565,6 +557,19 @@ public final class XcodeInstaller { .map { xcode } } .then { xcode -> Promise in + return self.postInstallXcode(xcode) + } + } + + public func postInstallXcode(_ xcode: InstalledXcode) -> Promise { + let passwordInput = { + Promise { seal in + Current.logging.log("xcodes requires superuser privileges in order to finish installation.") + guard let password = Current.shell.readSecureLine(prompt: "macOS User Password: ") else { seal.reject(Error.missingSudoerPassword); return } + seal.fulfill(password + "\n") + } + } + return firstly { () -> Promise in Current.logging.log(InstallationStep.finishing.description) return self.enableDeveloperMode(passwordInput: passwordInput).map { xcode } From 2de75769d1fda04afd6f80344c2b7a878a0bd6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Lo=CC=81pez?= Date: Sun, 6 Feb 2022 13:33:26 +0100 Subject: [PATCH 04/10] Added `no-superuser` flag The steps that require superuser (root) permissions are optional, so we can add a flag to skip them. This also makes `xcodes` more interactive (we don't need to stop to ask the user for its credentials). This closes #177. --- Sources/XcodesKit/XcodeInstaller.swift | 17 +++++++++++------ Sources/xcodes/main.swift | 5 ++++- Tests/XcodesKitTests/XcodesKitTests.swift | 20 ++++++++++---------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index 069dcdc..69c3f11 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -163,9 +163,9 @@ public final class XcodeInstaller { case aria2(Path) } - public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false) -> Promise { + public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false, noSuperuser: Bool) -> Promise { return firstly { () -> Promise in - return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0, experimentalUnxip: experimentalUnxip) + return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser) } .done { xcode in Current.logging.log("\nXcode \(xcode.version.descriptionWithoutBuildMetadata) has been installed to \(xcode.path.string)".green) @@ -173,12 +173,12 @@ public final class XcodeInstaller { } } - private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool) -> Promise { + private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool, noSuperuser: Bool) -> 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, experimentalUnxip: experimentalUnxip) + return self.installArchivedXcode(xcode, at: url, to: destination, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser) } .recover { error -> Promise in switch error { @@ -195,7 +195,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, experimentalUnxip: experimentalUnxip) + return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: attemptNumber + 1, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser) } } default: @@ -528,7 +528,7 @@ public final class XcodeInstaller { } } - public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false) -> Promise { + public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false, noSuperuser: Bool) -> Promise { return firstly { () -> Promise in let destinationURL = destination.join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url switch archiveURL.pathExtension { @@ -557,6 +557,11 @@ public final class XcodeInstaller { .map { xcode } } .then { xcode -> Promise in + if noSuperuser { + Current.logging.log(InstallationStep.finishing.description) + Current.logging.log("Skipping asking for superuser privileges.") + return Promise.value(xcode) + } return self.postInstallXcode(xcode) } } diff --git a/Sources/xcodes/main.swift b/Sources/xcodes/main.swift index 91f818e..d4df002 100644 --- a/Sources/xcodes/main.swift +++ b/Sources/xcodes/main.swift @@ -185,6 +185,9 @@ struct Xcodes: ParsableCommand { @Flag(help: "Use the experimental unxip functionality. May speed up unarchiving by up to 2-3x.") var experimentalUnxip: Bool = false + + @Flag(help: "Don't ask for superuser (root) permission. Some optional steps of the installation will be skipped.") + var noSuperuser: Bool = false @Option(help: "The directory to install Xcode into. Defaults to /Applications.", completion: .directory) @@ -221,7 +224,7 @@ struct Xcodes: ParsableCommand { let destination = getDirectory(possibleDirectory: directory) - installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, experimentalUnxip: experimentalUnxip) + installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser) .done { Install.exit() } .catch { error in Install.processDownloadOrInstall(error: error) diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 947716e..2215b60 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"), noSuperuser: false) .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"), noSuperuser: false) .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"), noSuperuser: false) .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"), noSuperuser: false) .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"), noSuperuser: false) .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"), noSuperuser: false) .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"), noSuperuser: false) .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"), noSuperuser: false) .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"), noSuperuser: false) .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"), noSuperuser: false) .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) From e3bc8742e65f8d1cbfea15c9bcb6c5db79dd09f1 Mon Sep 17 00:00:00 2001 From: Ryan Pendleton Date: Tue, 23 Aug 2022 22:56:38 -0600 Subject: [PATCH 05/10] update unxip to the latest version --- Sources/Unxip/Unxip.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/Unxip/Unxip.swift b/Sources/Unxip/Unxip.swift index a078bd6..b15a761 100644 --- a/Sources/Unxip/Unxip.swift +++ b/Sources/Unxip/Unxip.swift @@ -133,8 +133,9 @@ struct File { compressionStream.addTask { try Task.checkCancellation() let position = _position - let data = [UInt8](unsafeUninitializedCapacity: blockSize + blockSize / 16) { buffer, count in - data[position..(of path: S) -> S.SubSequence { - return path[.. Date: Sat, 10 Sep 2022 13:55:29 +0200 Subject: [PATCH 06/10] Added `--delete-xip` flag to `xcodes install` After Pull Request #60, `xcodes` always moves Xcode's .xip to the Trash after installation. This is problematic for scripts, because on modern macOS versions, [Full Disk Access is required to programatically delete the Trash](https://apple.stackexchange.com/questions/376916/cannot-ls-trash-in-the-terminal-in-catalina-operation-not-permitted). To solve this without reducing security, added a new `--delete-xip` flag that automatically deletes the .xip after a successful installation. - This is done this way to preserve the current behaviour (moving the .xip to the Trash) by default, [as it was originally intended](https://github.com/RobotsAndPencils/xcodes/issues/56#issuecomment-512919385). This closes #185. --- Sources/XcodesKit/XcodeInstaller.swift | 30 ++++++++++++++--------- Sources/xcodes/main.swift | 5 +++- Tests/XcodesKitTests/XcodesKitTests.swift | 20 +++++++-------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index 69c3f11..a6d2d8c 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -82,7 +82,7 @@ public final class XcodeInstaller { case downloading(version: String, progress: String?, willInstall: Bool) case unarchiving(experimentalUnxip: Bool) case moving(destination: String) - case trashingArchive(archiveName: String) + case cleaningArchive(archiveName: String, shouldDelete: Bool) case checkingSecurity case finishing @@ -114,7 +114,10 @@ public final class XcodeInstaller { """ case .moving(let destination): return "Moving Xcode to \(destination)" - case .trashingArchive(let archiveName): + case .cleaningArchive(let archiveName, let shouldDelete): + if shouldDelete { + return "Deleting Xcode archive \(archiveName)" + } return "Moving Xcode archive \(archiveName) to the Trash" case .checkingSecurity: return "Checking security assessment and code signing" @@ -128,7 +131,7 @@ public final class XcodeInstaller { case .downloading: return 1 case .unarchiving: return 2 case .moving: return 3 - case .trashingArchive: return 4 + case .cleaningArchive: return 4 case .checkingSecurity: return 5 case .finishing: return 6 } @@ -163,9 +166,9 @@ public final class XcodeInstaller { case aria2(Path) } - public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false, noSuperuser: Bool) -> Promise { + public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false, deleteXip: Bool, noSuperuser: Bool) -> Promise { return firstly { () -> Promise in - return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser) + return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0, experimentalUnxip: experimentalUnxip, deleteXip: deleteXip, noSuperuser: noSuperuser) } .done { xcode in Current.logging.log("\nXcode \(xcode.version.descriptionWithoutBuildMetadata) has been installed to \(xcode.path.string)".green) @@ -173,12 +176,12 @@ public final class XcodeInstaller { } } - private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool, noSuperuser: Bool) -> Promise { + private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool, deleteXip: Bool, noSuperuser: Bool) -> 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, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser) + return self.installArchivedXcode(xcode, at: url, to: destination, experimentalUnxip: experimentalUnxip, deleteXip: deleteXip, noSuperuser: noSuperuser) } .recover { error -> Promise in switch error { @@ -195,7 +198,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, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser) + return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: attemptNumber + 1, experimentalUnxip: experimentalUnxip, deleteXip: deleteXip, noSuperuser: noSuperuser) } } default: @@ -528,7 +531,7 @@ public final class XcodeInstaller { } } - public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false, noSuperuser: Bool) -> Promise { + public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false, deleteXip: Bool, noSuperuser: Bool) -> Promise { return firstly { () -> Promise in let destinationURL = destination.join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url switch archiveURL.pathExtension { @@ -548,8 +551,13 @@ public final class XcodeInstaller { } } .then { xcode -> Promise in - Current.logging.log(InstallationStep.trashingArchive(archiveName: archiveURL.lastPathComponent).description) - try Current.files.trashItem(at: archiveURL) + Current.logging.log(InstallationStep.cleaningArchive(archiveName: archiveURL.lastPathComponent, shouldDelete: deleteXip).description) + if deleteXip { + try Current.files.removeItem(at: archiveURL) + } + else { + try Current.files.trashItem(at: archiveURL) + } Current.logging.log(InstallationStep.checkingSecurity.description) return when(fulfilled: self.verifySecurityAssessment(of: xcode), diff --git a/Sources/xcodes/main.swift b/Sources/xcodes/main.swift index d4df002..a3e6840 100644 --- a/Sources/xcodes/main.swift +++ b/Sources/xcodes/main.swift @@ -189,6 +189,9 @@ struct Xcodes: ParsableCommand { @Flag(help: "Don't ask for superuser (root) permission. Some optional steps of the installation will be skipped.") var noSuperuser: Bool = false + @Flag(help: "Completely delete Xcode .xip after installation, instead of moving it to the user's Trash.") + var deleteXip: Bool = false + @Option(help: "The directory to install Xcode into. Defaults to /Applications.", completion: .directory) var directory: String? @@ -224,7 +227,7 @@ struct Xcodes: ParsableCommand { let destination = getDirectory(possibleDirectory: directory) - installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser) + installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, experimentalUnxip: experimentalUnxip, deleteXip: deleteXip, noSuperuser: noSuperuser) .done { Install.exit() } .catch { error in Install.processDownloadOrInstall(error: error) diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 2215b60..cce59b5 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"), noSuperuser: false) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.installArchivedXcode(xcode, at: xipURL, to: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.home.join("Xcode"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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"), noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), deleteXip: false, noSuperuser: false) .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) From 49d5e82621dc690151f240f4af1d95cce7617c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Lo=CC=81pez?= Date: Sat, 10 Sep 2022 17:01:01 +0200 Subject: [PATCH 07/10] Added `--delete-app` flag to `xcodes uninstall` This is equivalent to the `--delete-xip` flag that was added to `xcodes install` on the previous commit. --- Sources/XcodesKit/XcodeInstaller.swift | 21 ++++++++++++++++----- Sources/xcodes/main.swift | 5 ++++- Tests/XcodesKitTests/XcodesKitTests.swift | 4 ++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index a6d2d8c..d7bcb2d 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -595,7 +595,7 @@ public final class XcodeInstaller { } } - public func uninstallXcode(_ versionString: String, directory: Path) -> Promise { + public func uninstallXcode(_ versionString: String, directory: Path, deleteApp: Bool) -> Promise { return firstly { () -> Promise in guard let version = Version(xcodeVersion: versionString) else { Current.logging.log(Error.invalidVersion(versionString).legibleLocalizedDescription) @@ -609,11 +609,17 @@ public final class XcodeInstaller { return Promise.value(installedXcode) } - .map { ($0, try Current.files.trashItem(at: $0.path.url)) } - .then { (installedXcode, trashURL) -> Promise<(InstalledXcode, URL)> in + .map { installedXcode -> (InstalledXcode, URL?) in + if deleteApp { + try Current.files.removeItem(at: installedXcode.path.url) + return (installedXcode, nil) + } + return (installedXcode, try Current.files.trashItem(at: installedXcode.path.url)) + } + .then { (installedXcode, trashURL) -> Promise<(InstalledXcode, URL?)> in // If we just uninstalled the selected Xcode, try to select the latest installed version so things don't accidentally break Current.shell.xcodeSelectPrintPath() - .then { output -> Promise<(InstalledXcode, URL)> in + .then { output -> Promise<(InstalledXcode, URL?)> in if output.out.hasPrefix(installedXcode.path.string), let latestInstalledXcode = Current.files.installedXcodes(directory).sorted(by: { $0.version < $1.version }).last { return selectXcodeAtPath(latestInstalledXcode.path.string) @@ -628,7 +634,12 @@ public final class XcodeInstaller { } } .done { (installedXcode, trashURL) in - Current.logging.log("Xcode \(installedXcode.version.appleDescription) moved to Trash: \(trashURL.path)".green) + if let trashURL = trashURL { + Current.logging.log("Xcode \(installedXcode.version.appleDescription) moved to Trash: \(trashURL.path)".green) + } + else { + Current.logging.log("Xcode \(installedXcode.version.appleDescription) deleted".green) + } Current.shell.exit(0) } } diff --git a/Sources/xcodes/main.swift b/Sources/xcodes/main.swift index a3e6840..cc7f925 100644 --- a/Sources/xcodes/main.swift +++ b/Sources/xcodes/main.swift @@ -351,6 +351,9 @@ struct Xcodes: ParsableCommand { completion: .custom { _ in Current.files.installedXcodes(getDirectory(possibleDirectory: nil)).sorted { $0.version < $1.version }.map { $0.version.appleDescription } }) var version: [String] = [] + @Flag(help: "Completely delete Xcode, instead of moving it to the user's Trash.") + var deleteApp: Bool = false + @OptionGroup var globalDirectory: GlobalDirectoryOption @@ -362,7 +365,7 @@ struct Xcodes: ParsableCommand { let directory = getDirectory(possibleDirectory: globalDirectory.directory) - installer.uninstallXcode(version.joined(separator: " "), directory: directory) + installer.uninstallXcode(version.joined(separator: " "), directory: directory, deleteApp: deleteApp) .done { Uninstall.exit() } .catch { error in Uninstall.exit(withLegibleError: error) } diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index cce59b5..42a2477 100644 --- a/Tests/XcodesKitTests/XcodesKitTests.swift +++ b/Tests/XcodesKitTests/XcodesKitTests.swift @@ -778,7 +778,7 @@ final class XcodesKitTests: XCTestCase { return Promise.value((status: 0, out: "", err: "")) } - installer.uninstallXcode("0.0.0", directory: Path.root.join("Applications")) + installer.uninstallXcode("0.0.0", directory: Path.root.join("Applications"), deleteApp: false) .ensure { XCTAssertEqual(selectedPaths, ["/Applications/Xcode-2.0.1.app"]) XCTAssertEqual(trashedItemAtURL, installedXcodes[0].path.url) @@ -823,7 +823,7 @@ final class XcodesKitTests: XCTestCase { return URL(fileURLWithPath: "\(NSHomeDirectory())/.Trash/\(itemURL.lastPathComponent)") } - installer.uninstallXcode("999", directory: Path.root.join("Applications")) + installer.uninstallXcode("999", directory: Path.root.join("Applications"), deleteApp: false) .ensure { XCTAssertEqual(trashedItemAtURL, installedXcodes[0].path.url) } From c49767b1f08f54428b2c273fef17f59a9f45a483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Lo=CC=81pez?= Date: Sat, 10 Sep 2022 17:15:20 +0200 Subject: [PATCH 08/10] Renamed `--delete-xip` and `--delete-app` flags to `--empty-trash` This two flags do essentially the same thing (skip the Trash and irrevocably delete Xcode or its .xip) so it makes sense to use the same name for both. - Although the Trash isn't really "emptied" (the files never get to the Trash in the first place), this is the most intuitively way I've found to refer to this at a high level. - Alternatives considered: - `--skip-trash`. This explains better the current implementation, but I think it's more ambiguous: if the user doesn't know that by default files are moved to the Trash, "skip Trash" could be understood as "skip deleting files from the Trash"... - `--delete-immediately` (the way Finder refers to deleting a single file from the Trash). This is longer to type, and "immediately" could imply that, without this flag, files will be automatically deleted in the future somehow. --- Sources/XcodesKit/XcodeInstaller.swift | 20 +++++++++---------- Sources/xcodes/main.swift | 12 ++++++------ Tests/XcodesKitTests/XcodesKitTests.swift | 24 +++++++++++------------ 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index d7bcb2d..e2cf106 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -166,9 +166,9 @@ public final class XcodeInstaller { case aria2(Path) } - public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false, deleteXip: Bool, noSuperuser: Bool) -> Promise { + public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false, emptyTrash: Bool, noSuperuser: Bool) -> Promise { return firstly { () -> Promise in - return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0, experimentalUnxip: experimentalUnxip, deleteXip: deleteXip, noSuperuser: noSuperuser) + return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0, experimentalUnxip: experimentalUnxip, emptyTrash: emptyTrash, noSuperuser: noSuperuser) } .done { xcode in Current.logging.log("\nXcode \(xcode.version.descriptionWithoutBuildMetadata) has been installed to \(xcode.path.string)".green) @@ -176,12 +176,12 @@ public final class XcodeInstaller { } } - private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool, deleteXip: Bool, noSuperuser: Bool) -> Promise { + private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool, emptyTrash: Bool, noSuperuser: Bool) -> 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, experimentalUnxip: experimentalUnxip, deleteXip: deleteXip, noSuperuser: noSuperuser) + return self.installArchivedXcode(xcode, at: url, to: destination, experimentalUnxip: experimentalUnxip, emptyTrash: emptyTrash, noSuperuser: noSuperuser) } .recover { error -> Promise in switch error { @@ -198,7 +198,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, experimentalUnxip: experimentalUnxip, deleteXip: deleteXip, noSuperuser: noSuperuser) + return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: attemptNumber + 1, experimentalUnxip: experimentalUnxip, emptyTrash: emptyTrash, noSuperuser: noSuperuser) } } default: @@ -531,7 +531,7 @@ public final class XcodeInstaller { } } - public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false, deleteXip: Bool, noSuperuser: Bool) -> Promise { + public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false, emptyTrash: Bool, noSuperuser: Bool) -> Promise { return firstly { () -> Promise in let destinationURL = destination.join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url switch archiveURL.pathExtension { @@ -551,8 +551,8 @@ public final class XcodeInstaller { } } .then { xcode -> Promise in - Current.logging.log(InstallationStep.cleaningArchive(archiveName: archiveURL.lastPathComponent, shouldDelete: deleteXip).description) - if deleteXip { + Current.logging.log(InstallationStep.cleaningArchive(archiveName: archiveURL.lastPathComponent, shouldDelete: emptyTrash).description) + if emptyTrash { try Current.files.removeItem(at: archiveURL) } else { @@ -595,7 +595,7 @@ public final class XcodeInstaller { } } - public func uninstallXcode(_ versionString: String, directory: Path, deleteApp: Bool) -> Promise { + public func uninstallXcode(_ versionString: String, directory: Path, emptyTrash: Bool) -> Promise { return firstly { () -> Promise in guard let version = Version(xcodeVersion: versionString) else { Current.logging.log(Error.invalidVersion(versionString).legibleLocalizedDescription) @@ -610,7 +610,7 @@ public final class XcodeInstaller { return Promise.value(installedXcode) } .map { installedXcode -> (InstalledXcode, URL?) in - if deleteApp { + if emptyTrash { try Current.files.removeItem(at: installedXcode.path.url) return (installedXcode, nil) } diff --git a/Sources/xcodes/main.swift b/Sources/xcodes/main.swift index cc7f925..c98034e 100644 --- a/Sources/xcodes/main.swift +++ b/Sources/xcodes/main.swift @@ -189,8 +189,8 @@ struct Xcodes: ParsableCommand { @Flag(help: "Don't ask for superuser (root) permission. Some optional steps of the installation will be skipped.") var noSuperuser: Bool = false - @Flag(help: "Completely delete Xcode .xip after installation, instead of moving it to the user's Trash.") - var deleteXip: Bool = false + @Flag(help: "Completely delete Xcode .xip after installation, instead of keeping it on the user's Trash.") + var emptyTrash: Bool = false @Option(help: "The directory to install Xcode into. Defaults to /Applications.", completion: .directory) @@ -227,7 +227,7 @@ struct Xcodes: ParsableCommand { let destination = getDirectory(possibleDirectory: directory) - installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, experimentalUnxip: experimentalUnxip, deleteXip: deleteXip, noSuperuser: noSuperuser) + installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, experimentalUnxip: experimentalUnxip, emptyTrash: emptyTrash, noSuperuser: noSuperuser) .done { Install.exit() } .catch { error in Install.processDownloadOrInstall(error: error) @@ -351,8 +351,8 @@ struct Xcodes: ParsableCommand { completion: .custom { _ in Current.files.installedXcodes(getDirectory(possibleDirectory: nil)).sorted { $0.version < $1.version }.map { $0.version.appleDescription } }) var version: [String] = [] - @Flag(help: "Completely delete Xcode, instead of moving it to the user's Trash.") - var deleteApp: Bool = false + @Flag(help: "Completely delete Xcode, instead of keeping it on the user's Trash.") + var emptyTrash: Bool = false @OptionGroup var globalDirectory: GlobalDirectoryOption @@ -365,7 +365,7 @@ struct Xcodes: ParsableCommand { let directory = getDirectory(possibleDirectory: globalDirectory.directory) - installer.uninstallXcode(version.joined(separator: " "), directory: directory, deleteApp: deleteApp) + installer.uninstallXcode(version.joined(separator: " "), directory: directory, emptyTrash: emptyTrash) .done { Uninstall.exit() } .catch { error in Uninstall.exit(withLegibleError: error) } diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 42a2477..91da13c 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"), deleteXip: false, noSuperuser: false) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.installArchivedXcode(xcode, at: xipURL, to: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.home.join("Xcode"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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"), deleteXip: false, noSuperuser: false) + installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), emptyTrash: false, noSuperuser: false) .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) @@ -778,7 +778,7 @@ final class XcodesKitTests: XCTestCase { return Promise.value((status: 0, out: "", err: "")) } - installer.uninstallXcode("0.0.0", directory: Path.root.join("Applications"), deleteApp: false) + installer.uninstallXcode("0.0.0", directory: Path.root.join("Applications"), emptyTrash: false) .ensure { XCTAssertEqual(selectedPaths, ["/Applications/Xcode-2.0.1.app"]) XCTAssertEqual(trashedItemAtURL, installedXcodes[0].path.url) @@ -823,7 +823,7 @@ final class XcodesKitTests: XCTestCase { return URL(fileURLWithPath: "\(NSHomeDirectory())/.Trash/\(itemURL.lastPathComponent)") } - installer.uninstallXcode("999", directory: Path.root.join("Applications"), deleteApp: false) + installer.uninstallXcode("999", directory: Path.root.join("Applications"), emptyTrash: false) .ensure { XCTAssertEqual(trashedItemAtURL, installedXcodes[0].path.url) } From e3bfc58dbda30955970058752e6291252d4dbc1f Mon Sep 17 00:00:00 2001 From: Ahmet Geymen Date: Thu, 15 Sep 2022 01:22:27 +0300 Subject: [PATCH 09/10] Update Makefile fixed: "warning: '--build-path' option is deprecated; use '--scratch-path' instead" --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d95f808..5824af2 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ xcodes: $(SOURCES) --configuration release \ -Xswiftc -Onone \ --disable-sandbox \ - --build-path "$(BUILDDIR)" \ + --scratch-path "$(BUILDDIR)" \ --arch arm64 \ --arch x86_64 \ From d18bf481c38a7933611a23d83fa10896d655d3ca Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Sat, 17 Sep 2022 19:51:29 -0500 Subject: [PATCH 10/10] Bump version to 1.0.0 --- Makefile | 2 +- Sources/XcodesKit/Version.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5824af2..d95f808 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ xcodes: $(SOURCES) --configuration release \ -Xswiftc -Onone \ --disable-sandbox \ - --scratch-path "$(BUILDDIR)" \ + --build-path "$(BUILDDIR)" \ --arch arm64 \ --arch x86_64 \ diff --git a/Sources/XcodesKit/Version.swift b/Sources/XcodesKit/Version.swift index e722549..23a5691 100644 --- a/Sources/XcodesKit/Version.swift +++ b/Sources/XcodesKit/Version.swift @@ -1,3 +1,3 @@ import Version -public let version = Version("0.20.0")! +public let version = Version("1.0.0")!