Skip to content

Commit c979aa4

Browse files
authored
Merge pull request #207 from juanjonol/no-superuser
Added `no-superuser` flag
2 parents 42d8544 + 2de7576 commit c979aa4

File tree

3 files changed

+38
-25
lines changed

3 files changed

+38
-25
lines changed

Sources/XcodesKit/XcodeInstaller.swift

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -163,22 +163,22 @@ public final class XcodeInstaller {
163163
case aria2(Path)
164164
}
165165

166-
public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false) -> Promise<Void> {
166+
public func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, experimentalUnxip: Bool = false, noSuperuser: Bool) -> Promise<Void> {
167167
return firstly { () -> Promise<InstalledXcode> in
168-
return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0, experimentalUnxip: experimentalUnxip)
168+
return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: 0, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser)
169169
}
170170
.done { xcode in
171171
Current.logging.log("\nXcode \(xcode.version.descriptionWithoutBuildMetadata) has been installed to \(xcode.path.string)".green)
172172
Current.shell.exit(0)
173173
}
174174
}
175175

176-
private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool) -> Promise<InstalledXcode> {
176+
private func install(_ installationType: InstallationType, dataSource: DataSource, downloader: Downloader, destination: Path, attemptNumber: Int, experimentalUnxip: Bool, noSuperuser: Bool) -> Promise<InstalledXcode> {
177177
return firstly { () -> Promise<(Xcode, URL)> in
178178
return self.getXcodeArchive(installationType, dataSource: dataSource, downloader: downloader, destination: destination, willInstall: true)
179179
}
180180
.then { xcode, url -> Promise<InstalledXcode> in
181-
return self.installArchivedXcode(xcode, at: url, to: destination, experimentalUnxip: experimentalUnxip)
181+
return self.installArchivedXcode(xcode, at: url, to: destination, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser)
182182
}
183183
.recover { error -> Promise<InstalledXcode> in
184184
switch error {
@@ -195,7 +195,7 @@ public final class XcodeInstaller {
195195
Current.logging.log(error.legibleLocalizedDescription.red)
196196
Current.logging.log("Removing damaged XIP and re-attempting installation.\n")
197197
try Current.files.removeItem(at: damagedXIPURL)
198-
return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: attemptNumber + 1, experimentalUnxip: experimentalUnxip)
198+
return self.install(installationType, dataSource: dataSource, downloader: downloader, destination: destination, attemptNumber: attemptNumber + 1, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser)
199199
}
200200
}
201201
default:
@@ -528,15 +528,7 @@ public final class XcodeInstaller {
528528
}
529529
}
530530

531-
public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false) -> Promise<InstalledXcode> {
532-
let passwordInput = {
533-
Promise<String> { seal in
534-
Current.logging.log("xcodes requires superuser privileges in order to finish installation.")
535-
guard let password = Current.shell.readSecureLine(prompt: "macOS User Password: ") else { seal.reject(Error.missingSudoerPassword); return }
536-
seal.fulfill(password + "\n")
537-
}
538-
}
539-
531+
public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path, experimentalUnxip: Bool = false, noSuperuser: Bool) -> Promise<InstalledXcode> {
540532
return firstly { () -> Promise<InstalledXcode> in
541533
let destinationURL = destination.join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url
542534
switch archiveURL.pathExtension {
@@ -565,6 +557,24 @@ public final class XcodeInstaller {
565557
.map { xcode }
566558
}
567559
.then { xcode -> Promise<InstalledXcode> in
560+
if noSuperuser {
561+
Current.logging.log(InstallationStep.finishing.description)
562+
Current.logging.log("Skipping asking for superuser privileges.")
563+
return Promise.value(xcode)
564+
}
565+
return self.postInstallXcode(xcode)
566+
}
567+
}
568+
569+
public func postInstallXcode(_ xcode: InstalledXcode) -> Promise<InstalledXcode> {
570+
let passwordInput = {
571+
Promise<String> { seal in
572+
Current.logging.log("xcodes requires superuser privileges in order to finish installation.")
573+
guard let password = Current.shell.readSecureLine(prompt: "macOS User Password: ") else { seal.reject(Error.missingSudoerPassword); return }
574+
seal.fulfill(password + "\n")
575+
}
576+
}
577+
return firstly { () -> Promise<InstalledXcode> in
568578
Current.logging.log(InstallationStep.finishing.description)
569579

570580
return self.enableDeveloperMode(passwordInput: passwordInput).map { xcode }

Sources/xcodes/main.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ struct Xcodes: ParsableCommand {
185185

186186
@Flag(help: "Use the experimental unxip functionality. May speed up unarchiving by up to 2-3x.")
187187
var experimentalUnxip: Bool = false
188+
189+
@Flag(help: "Don't ask for superuser (root) permission. Some optional steps of the installation will be skipped.")
190+
var noSuperuser: Bool = false
188191

189192
@Option(help: "The directory to install Xcode into. Defaults to /Applications.",
190193
completion: .directory)
@@ -221,7 +224,7 @@ struct Xcodes: ParsableCommand {
221224

222225
let destination = getDirectory(possibleDirectory: directory)
223226

224-
installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, experimentalUnxip: experimentalUnxip)
227+
installer.install(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destination: destination, experimentalUnxip: experimentalUnxip, noSuperuser: noSuperuser)
225228
.done { Install.exit() }
226229
.catch { error in
227230
Install.processDownloadOrInstall(error: error)

Tests/XcodesKitTests/XcodesKitTests.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,23 +86,23 @@ final class XcodesKitTests: XCTestCase {
8686

8787
let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil)
8888
let installedXcode = InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!
89-
installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"))
89+
installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), noSuperuser: false)
9090
.catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.failedSecurityAssessment(xcode: installedXcode, output: "")) }
9191
}
9292

9393
func test_InstallArchivedXcode_VerifySigningCertificateFails_Throws() {
9494
Current.shell.codesignVerify = { _ in return Promise(error: Process.PMKError.execution(process: Process(), standardOutput: nil, standardError: nil)) }
9595

9696
let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil)
97-
installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"))
97+
installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), noSuperuser: false)
9898
.catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.codesignVerifyFailed(output: "")) }
9999
}
100100

101101
func test_InstallArchivedXcode_VerifySigningCertificateDoesntMatch_Throws() {
102102
Current.shell.codesignVerify = { _ in return Promise.value((0, "", "")) }
103103

104104
let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil)
105-
installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"))
105+
installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), to: Path.root.join("Applications"), noSuperuser: false)
106106
.catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.unexpectedCodeSigningIdentity(identifier: "", certificateAuthority: [])) }
107107
}
108108

@@ -115,7 +115,7 @@ final class XcodesKitTests: XCTestCase {
115115

116116
let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil)
117117
let xipURL = URL(fileURLWithPath: "/Xcode-0.0.0.xip")
118-
installer.installArchivedXcode(xcode, at: xipURL, to: Path.root.join("Applications"))
118+
installer.installArchivedXcode(xcode, at: xipURL, to: Path.root.join("Applications"), noSuperuser: false)
119119
.ensure { XCTAssertEqual(trashedItemAtURL, xipURL) }
120120
.cauterize()
121121
}
@@ -203,7 +203,7 @@ final class XcodesKitTests: XCTestCase {
203203

204204
let expectation = self.expectation(description: "Finished")
205205

206-
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"))
206+
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), noSuperuser: false)
207207
.ensure {
208208
let url = Bundle.module.url(forResource: "LogOutput-FullHappyPath", withExtension: "txt", subdirectory: "Fixtures")!
209209
XCTAssertEqual(log, try! String(contentsOf: url))
@@ -296,7 +296,7 @@ final class XcodesKitTests: XCTestCase {
296296

297297
let expectation = self.expectation(description: "Finished")
298298

299-
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"))
299+
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), noSuperuser: false)
300300
.ensure {
301301
let url = Bundle.module.url(forResource: "LogOutput-FullHappyPath-NoColor", withExtension: "txt", subdirectory: "Fixtures")!
302302
XCTAssertEqual(log, try! String(contentsOf: url))
@@ -393,7 +393,7 @@ final class XcodesKitTests: XCTestCase {
393393

394394
let expectation = self.expectation(description: "Finished")
395395

396-
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"))
396+
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), noSuperuser: false)
397397
.ensure {
398398
let url = Bundle.module.url(forResource: "LogOutput-FullHappyPath-NonInteractiveTerminal", withExtension: "txt", subdirectory: "Fixtures")!
399399
XCTAssertEqual(log, try! String(contentsOf: url))
@@ -486,7 +486,7 @@ final class XcodesKitTests: XCTestCase {
486486

487487
let expectation = self.expectation(description: "Finished")
488488

489-
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.home.join("Xcode"))
489+
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.home.join("Xcode"), noSuperuser: false)
490490
.ensure {
491491
let url = Bundle.module.url(forResource: "LogOutput-AlternativeDirectory", withExtension: "txt", subdirectory: "Fixtures")!
492492
let expectedText = try! String(contentsOf: url).replacingOccurrences(of: "/Users/brandon", with: Path.home.string)
@@ -600,7 +600,7 @@ final class XcodesKitTests: XCTestCase {
600600

601601
let expectation = self.expectation(description: "Finished")
602602

603-
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"))
603+
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), noSuperuser: false)
604604
.ensure {
605605
let url = Bundle.module.url(forResource: "LogOutput-IncorrectSavedPassword", withExtension: "txt", subdirectory: "Fixtures")!
606606
XCTAssertEqual(log, try! String(contentsOf: url))
@@ -718,7 +718,7 @@ final class XcodesKitTests: XCTestCase {
718718

719719
let expectation = self.expectation(description: "Finished")
720720

721-
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"))
721+
installer.install(.version("0.0.0"), dataSource: .apple, downloader: .urlSession, destination: Path.root.join("Applications"), noSuperuser: false)
722722
.ensure {
723723
let url = Bundle.module.url(forResource: "LogOutput-DamagedXIP", withExtension: "txt", subdirectory: "Fixtures")!
724724
let expectedText = try! String(contentsOf: url).replacingOccurrences(of: "/Users/brandon", with: Path.home.string)

0 commit comments

Comments
 (0)