Skip to content
Open
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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ xcodes will download and install the version you asked for so that it's ready to
(5/6) Checking security assessment and code signing
(6/6) Finishing installation
xcodes requires superuser privileges in order to finish installation.
macOS User Password:

Xcode 11.2.0 has been installed to /Applications/Xcode-11.2.0.app
```
Expand Down
27 changes: 8 additions & 19 deletions Sources/XcodesKit/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,22 @@ public struct Shell {
public var installRuntimeImage: (URL) -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "add", $0.path) }
public var spctlAssess: (URL) -> Promise<ProcessOutput> = { Process.run(Path.root.usr.sbin.spctl, "--assess", "--verbose", "--type", "execute", "\($0.path)") }
public var codesignVerify: (URL) -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.codesign, "-vv", "-d", "\($0.path)") }
public var devToolsSecurityEnable: (String?) -> Promise<ProcessOutput> = { Process.sudo(password: $0, Path.root.usr.sbin.DevToolsSecurity, "-enable") }
public var addStaffToDevelopersGroup: (String?) -> Promise<ProcessOutput> = { Process.sudo(password: $0, Path.root.usr.sbin.dseditgroup, "-o", "edit", "-t", "group", "-a", "staff", "_developer") }
public var acceptXcodeLicense: (InstalledXcode, String?) -> Promise<ProcessOutput> = { Process.sudo(password: $1, $0.path.join("/Contents/Developer/usr/bin/xcodebuild"), "-license", "accept") }
public var runFirstLaunch: (InstalledXcode, String?) -> Promise<ProcessOutput> = { Process.sudo(password: $1, $0.path.join("/Contents/Developer/usr/bin/xcodebuild"),"-runFirstLaunch") }
public var devToolsSecurityEnable: () -> Promise<ProcessOutput> = { Process.sudo(Path.root.usr.sbin.DevToolsSecurity, "-enable") }
public var addStaffToDevelopersGroup: () -> Promise<ProcessOutput> = { Process.sudo(Path.root.usr.sbin.dseditgroup, "-o", "edit", "-t", "group", "-a", "staff", "_developer") }
public var acceptXcodeLicense: (InstalledXcode) -> Promise<ProcessOutput> = { Process.sudo($0.path.join("/Contents/Developer/usr/bin/xcodebuild"), "-license", "accept") }
public var runFirstLaunch: (InstalledXcode) -> Promise<ProcessOutput> = { Process.sudo($0.path.join("/Contents/Developer/usr/bin/xcodebuild"),"-runFirstLaunch") }
public var buildVersion: () -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.sw_vers, "-buildVersion") }
public var xcodeBuildVersion: (InstalledXcode) -> Promise<ProcessOutput> = { Process.run(Path.root.usr.libexec.PlistBuddy, "-c", "Print :ProductBuildVersion", "\($0.path.string)/Contents/version.plist") }
public var getUserCacheDir: () -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.getconf, "DARWIN_USER_CACHE_DIR") }
public var touchInstallCheck: (String, String, String) -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin/"touch", "\($0)com.apple.dt.Xcode.InstallCheckCache_\($1)_\($2)") }
public var installedRuntimes: () -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "list", "-j") }

public var validateSudoAuthentication: () -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.sudo, "-nv") }
public var authenticateSudoerIfNecessary: (@escaping () -> Promise<String>) -> Promise<String?> = { passwordInput in
firstly { () -> Promise<String?> in
Current.shell.validateSudoAuthentication().map { _ in return nil }
}
.recover { _ -> Promise<String?> in
return passwordInput().map(Optional.init)
}
}
public func authenticateSudoerIfNecessary(passwordInput: @escaping () -> Promise<String>) -> Promise<String?> {
authenticateSudoerIfNecessary(passwordInput)
}
public var authenticateSudoer: () -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.sudo, "-v") }

public var xcodeSelectPrintPath: () -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.join("xcode-select"), "-p") }
public var xcodeSelectSwitch: (String?, String) -> Promise<ProcessOutput> = { Process.sudo(password: $0, Path.root.usr.bin.join("xcode-select"), "-s", $1) }
public func xcodeSelectSwitch(password: String?, path: String) -> Promise<ProcessOutput> {
xcodeSelectSwitch(password, path)
public var xcodeSelectSwitch: (String) -> Promise<ProcessOutput> = { Process.sudo(Path.root.usr.bin.join("xcode-select"), "-s", $0) }
public func xcodeSelectSwitch(path: String) -> Promise<ProcessOutput> {
xcodeSelectSwitch(path)
}
public var isRoot: () -> Bool = { NSUserName() == "root" }

Expand Down
8 changes: 2 additions & 6 deletions Sources/XcodesKit/Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ public typealias ProcessOutput = (status: Int32, out: String, err: String)

extension Process {
@discardableResult
static func sudo(password: String? = nil, _ executable: Path, workingDirectory: URL? = nil, _ arguments: String...) -> Promise<ProcessOutput> {
var arguments = [executable.string] + arguments
if password != nil {
arguments.insert("-S", at: 0)
}
return run(Path.root.usr.bin.sudo.url, workingDirectory: workingDirectory, input: password, arguments)
static func sudo(_ executable: Path, workingDirectory: URL? = nil, _ arguments: String...) -> Promise<ProcessOutput> {
run(Path.root.usr.bin.sudo.url, workingDirectory: workingDirectory, [executable.string] + arguments)
}

@discardableResult
Expand Down
51 changes: 17 additions & 34 deletions Sources/XcodesKit/XcodeInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public final class XcodeInstaller {
case codesignVerifyFailed(output: String)
case unexpectedCodeSigningIdentity(identifier: String, certificateAuthority: [String])
case unsupportedFileFormat(extension: String)
case missingSudoerPassword
case unavailableVersion(Version)
case noReleaseVersionAvailable
case noPrereleaseVersionAvailable
Expand Down Expand Up @@ -57,8 +56,6 @@ public final class XcodeInstaller {
"""
case .unsupportedFileFormat(let fileExtension):
return "xcodes doesn't (yet) support installing Xcode from the \(fileExtension) file format."
case .missingSudoerPassword:
return "Missing password. Please try again."
case let .unavailableVersion(version):
return "Could not find version \(version.appleDescription)."
case .noReleaseVersionAvailable:
Expand Down Expand Up @@ -421,23 +418,20 @@ public final class XcodeInstaller {
}

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)
Current.logging.log("xcodes requires superuser privileges in order to finish installation.")

return self.enableDeveloperMode(passwordInput: passwordInput).map { xcode }
return Current.shell.authenticateSudoer().asVoid().map { xcode }
}
.then { xcode -> Promise<InstalledXcode> in
self.enableDeveloperMode().map { xcode }
}
.then { xcode -> Promise<InstalledXcode> in
self.approveLicense(for: xcode, passwordInput: passwordInput).map { xcode }
self.approveLicense(for: xcode).map { xcode }
}
.then { xcode -> Promise<InstalledXcode> in
self.installComponents(for: xcode, passwordInput: passwordInput).map { xcode }
self.installComponents(for: xcode).map { xcode }
}
}

Expand Down Expand Up @@ -722,33 +716,22 @@ public final class XcodeInstaller {
return info
}

func enableDeveloperMode(passwordInput: @escaping () -> Promise<String>) -> Promise<Void> {
return firstly { () -> Promise<String?> in
Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput)
func enableDeveloperMode() -> Promise<Void> {
firstly {
Current.shell.devToolsSecurityEnable()
}
.then { possiblePassword -> Promise<String?> in
return Current.shell.devToolsSecurityEnable(possiblePassword).map { _ in possiblePassword }
}
.then { possiblePassword in
return Current.shell.addStaffToDevelopersGroup(possiblePassword).asVoid()
.then { _ in
Current.shell.addStaffToDevelopersGroup().asVoid()
}
}

func approveLicense(for xcode: InstalledXcode, passwordInput: @escaping () -> Promise<String>) -> Promise<Void> {
return firstly { () -> Promise<String?> in
Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput)
}
.then { possiblePassword in
return Current.shell.acceptXcodeLicense(xcode, possiblePassword).asVoid()
}
func approveLicense(for xcode: InstalledXcode) -> Promise<Void> {
Current.shell.acceptXcodeLicense(xcode).asVoid()
}

func installComponents(for xcode: InstalledXcode, passwordInput: @escaping () -> Promise<String>) -> Promise<Void> {
return firstly { () -> Promise<String?> in
Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput)
}
.then { possiblePassword -> Promise<Void> in
Current.shell.runFirstLaunch(xcode, possiblePassword).asVoid()
func installComponents(for xcode: InstalledXcode) -> Promise<Void> {
firstly {
Current.shell.runFirstLaunch(xcode).asVoid()
}
.then { () -> Promise<(String, String, String)> in
return when(fulfilled:
Expand Down
17 changes: 5 additions & 12 deletions Sources/XcodesKit/XcodeSelect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,23 +127,16 @@ public func selectXcodeInteractively(currentPath: String, directory: Path) -> Pr
}

public func selectXcodeAtPath(_ pathString: String) -> Promise<ProcessOutput> {
firstly { () -> Promise<String?> in
firstly {
guard Current.files.fileExists(atPath: pathString) else {
throw XcodeSelectError.invalidPath(pathString)
}

let passwordInput = {
Promise<String> { seal in
Current.logging.log("xcodes requires superuser privileges to select an Xcode")
guard let password = Current.shell.readSecureLine(prompt: "macOS User Password: ") else { seal.reject(XcodeInstaller.Error.missingSudoerPassword); return }
seal.fulfill(password + "\n")
}
}

return Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput)
Current.logging.log("xcodes requires superuser privileges to select an Xcode")
return Current.shell.authenticateSudoer().asVoid()
}
.then { possiblePassword in
Current.shell.xcodeSelectSwitch(password: possiblePassword, path: pathString)
.then {
Current.shell.xcodeSelectSwitch(path: pathString)
}
.then { _ in
Current.shell.xcodeSelectPrintPath()
Expand Down
13 changes: 6 additions & 7 deletions Tests/XcodesKitTests/Environment+Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@ extension Shell {
installRuntimeImage: { _ in return Promise.value(Shell.processOutputMock) },
spctlAssess: { _ in return Promise.value(Shell.processOutputMock) },
codesignVerify: { _ in return Promise.value(Shell.processOutputMock) },
devToolsSecurityEnable: { _ in return Promise.value(Shell.processOutputMock) },
addStaffToDevelopersGroup: { _ in return Promise.value(Shell.processOutputMock) },
acceptXcodeLicense: { _, _ in return Promise.value(Shell.processOutputMock) },
runFirstLaunch: { _, _ in return Promise.value(Shell.processOutputMock) },
devToolsSecurityEnable: { return Promise.value(Shell.processOutputMock) },
addStaffToDevelopersGroup: { return Promise.value(Shell.processOutputMock) },
acceptXcodeLicense: { _ in return Promise.value(Shell.processOutputMock) },
runFirstLaunch: { _ in return Promise.value(Shell.processOutputMock) },
buildVersion: { return Promise.value(Shell.processOutputMock) },
xcodeBuildVersion: { _ in return Promise.value(Shell.processOutputMock) },
getUserCacheDir: { return Promise.value(Shell.processOutputMock) },
touchInstallCheck: { _, _, _ in return Promise.value(Shell.processOutputMock) },
installedRuntimes: { return Promise.value(Shell.processOutputMock) },
validateSudoAuthentication: { return Promise.value(Shell.processOutputMock) },
// Deliberately using real implementation of authenticateSudoerIfNecessary since it depends on others that can be mocked
authenticateSudoer: { return Promise.value(Shell.processOutputMock) },
xcodeSelectPrintPath: { return Promise.value(Shell.processOutputMock) },
xcodeSelectSwitch: { _, _ in return Promise.value(Shell.processOutputMock) },
xcodeSelectSwitch: { _ in return Promise.value(Shell.processOutputMock) },
isRoot: { true },
readLine: { _ in return nil },
readSecureLine: { _, _ in return nil },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,5 @@ Using regular unxip. Try passing `--experimental-unxip` for a faster unxip proce
(5/6) Checking security assessment and code signing
(6/6) Finishing installation
xcodes requires superuser privileges in order to finish installation.
macOS User Password:

Xcode 0.0.0 has been installed to /Users/brandon/Xcode/Xcode-0.0.0.app
1 change: 0 additions & 1 deletion Tests/XcodesKitTests/Fixtures/LogOutput-DamagedXIP.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,5 @@ Using regular unxip. Try passing `--experimental-unxip` for a faster unxip proce
(5/6) Checking security assessment and code signing
(6/6) Finishing installation
xcodes requires superuser privileges in order to finish installation.
macOS User Password:

Xcode 0.0.0 has been installed to /Applications/Xcode-0.0.0.app
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,5 @@ Using regular unxip. Try passing `--experimental-unxip` for a faster unxip proce
(5/6) Checking security assessment and code signing
(6/6) Finishing installation
xcodes requires superuser privileges in order to finish installation.
macOS User Password:

Xcode 0.0.0 has been installed to /Applications/Xcode-0.0.0.app
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ Using regular unxip. Try passing `--experimental-unxip` for a faster unxip proce
(5/6) Checking security assessment and code signing
(6/6) Finishing installation
xcodes requires superuser privileges in order to finish installation.
macOS User Password:

Xcode 0.0.0 has been installed to /Applications/Xcode-0.0.0.app
1 change: 0 additions & 1 deletion Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,5 @@ Using regular unxip. Try passing `--experimental-unxip` for a faster unxip proce
(5/6) Checking security assessment and code signing
(6/6) Finishing installation
xcodes requires superuser privileges in order to finish installation.
macOS User Password:

Xcode 0.0.0 has been installed to /Applications/Xcode-0.0.0.app
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,5 @@ Using regular unxip. Try passing `--experimental-unxip` for a faster unxip proce
(5/6) Checking security assessment and code signing
(6/6) Finishing installation
xcodes requires superuser privileges in order to finish installation.
macOS User Password:

Xcode 0.0.0 has been installed to /Applications/Xcode-0.0.0.app
Loading