Skip to content
Merged
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
38 changes: 24 additions & 14 deletions Sources/XcodesKit/XcodeInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,22 +163,22 @@ public final class XcodeInstaller {
case aria2(Path)
}

public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false) -> Promise<Void> {
public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false, noSuperuser: Bool) -> Promise<Void> {
return firstly { () -> Promise<InstalledXcode> 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)
Current.shell.exit(0)
}
}

private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool) -> Promise<InstalledXcode> {
private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool, noSuperuser: Bool) -> Promise<InstalledXcode> {
return firstly { () -> Promise<(Xcode, URL)> in
return self.getXcodeArchive(installationType, dataSource: dataSource, downloader: downloader, destination: destination, willInstall: true)
}
.then { xcode, url -> Promise<InstalledXcode> 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<InstalledXcode> in
switch error {
Expand All @@ -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:
Expand Down Expand Up @@ -528,15 +528,7 @@ public final class XcodeInstaller {
}
}

public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false) -> Promise<InstalledXcode> {
let passwordInput = {
Promise<String> { 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")
}
}

public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false, noSuperuser: Bool) -> Promise<InstalledXcode> {
return firstly { () -> Promise<InstalledXcode> in
let destinationURL = destination.join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url
switch archiveURL.pathExtension {
Expand Down Expand Up @@ -565,6 +557,24 @@ public final class XcodeInstaller {
.map { xcode }
}
.then { xcode -> Promise<InstalledXcode> 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)
}
}

public func postInstallXcode(_ xcode: InstalledXcode) -> Promise<InstalledXcode> {
let passwordInput = {
Promise<String> { 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<InstalledXcode> in
Current.logging.log(InstallationStep.finishing.description)

return self.enableDeveloperMode(passwordInput: passwordInput).map { xcode }
Expand Down
5 changes: 4 additions & 1 deletion Sources/xcodes/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 10 additions & 10 deletions Tests/XcodesKitTests/XcodesKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,23 @@ 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: "")) }
}

func test_InstallArchivedXcode_VerifySigningCertificateFails_Throws() {
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: "")) }
}

func test_InstallArchivedXcode_VerifySigningCertificateDoesntMatch_Throws() {
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: [])) }
}

Expand All @@ -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()
}
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down