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
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ let package = Package(
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(
url: "https://github.com/onevcat/Rainbow",
from: "4.0.0"
),
.package(
url: "https://github.com/apple/swift-argument-parser",
from: "1.5.0"
Expand All @@ -36,6 +40,7 @@ let package = Package(
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "libunxip", package: "unxip"),
.product(name: "Rainbow", package: "Rainbow"),
.target(name: "TableKit")
]
),
Expand Down
28 changes: 25 additions & 3 deletions Sources/XcodeVersionManager/Commands/UseCommand.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ArgumentParser
import Foundation
import Rainbow

struct UseCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
Expand All @@ -22,7 +23,21 @@ struct UseCommand: AsyncParsableCommand {
let xcodeVersion = formatter.string(from: xcode)
print("Switching to \(xcodeVersion)")

try xcode.use(permissions: xcodeSelectPermissions)
do {
try xcode.use(permissions: xcodeSelectPermissions)
} catch let .sudoRequired(command) {
var standardError = StandardErrorOutputStream()
print(
"""
Requires superuser permissions to be run.
Manually run the following or see `xcvm use --help` for more details.
""".red,
to: &standardError
)

print(command)
throw ExitCode(1)
}
}
}

Expand All @@ -36,12 +51,19 @@ extension XcodeApplication.XcodeSelectPermissions: EnumerableFlag {
case .inherit:
ArgumentHelp(
"xcode-select will be run with same permissions xcvm has.",
discussion: #"Calling "sudo xcvm ..." may be required if you do not already have superuser permissions."#
discussion: """
Use one of the following based on you preference for granting superuser permissions.
- Copy the output commands and paste them into your terminal.
- `xcvm ... | bash` or `eval "$(xcvm ...)"`
Automatically evaluate the output commands.
- `sudo xcvm ...`
Give xcvm and its subprocesses superuser permissions.
"""
)

case .sudoAskpass:
ArgumentHelp(
#"xcvm will call xcode-select with "sudo --askpass ..."."#,
#"xcvm will call xcode-select with `sudo --askpass ...`."#,
discussion: """
The behaviour will be based on the system config.
1. If xcode-select has been added to sudoers with NOPASSWD, it won't need to prompt the user.
Expand Down
97 changes: 97 additions & 0 deletions Sources/XcodeVersionManager/Models/XcodeApplication+Use.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Foundation
import RegexBuilder

extension XcodeApplication {
enum XcodeSelectPermissions {
case inherit
case sudoAskpass
}

func use(permissions: XcodeSelectPermissions) throws(UseError) {
let xcodePath = url.absoluteURL.path

switch permissions {
case .inherit:
// Get the current user id to determine if being run as root
let userId = try execute(
"/usr/bin/id",
arguments: ["-u"]
)

let command = "/usr/bin/xcode-select"
let arguments = [
"--switch",
xcodePath
]

// Is root user
if String(decoding: userId, as: UTF8.self) == "0" {
// Works if xcvm is run with superuser permissions
try execute(command, arguments: arguments)
} else {
var command = "/usr/bin/sudo \(command)"
for argument in arguments {
let argument = if argument.contains(.whitespace) {
"'\(argument)'"
} else {
argument
}

command += " \(argument)"
}

throw .sudoRequired(command: command)
}
case .sudoAskpass:
// Works if
// xcode-select has NOPASSWD set in sudoers
// touchid is enabled for sudo
// SUDO_ASKPASS environment variable is set with a helper program
// otherwise fails to prompt for password
try execute(
"/usr/bin/sudo",
arguments: [
"--askpass",
"/usr/bin/xcode-select",
"--switch",
xcodePath
]
)
}

// Register Xcode so plugins work correctly.
// https://nshipster.com/xcode-source-extensions/#using-pluginkit
try execute(
"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
arguments: [
"-f",
xcodePath
]
)
}

@discardableResult
private func execute(_ command: String, arguments: [String]) throws(UseError) -> Data {
do {
return try Process.execute(command, arguments: arguments)
} catch {
throw .other(error)
}
}
}

extension XcodeApplication {
enum UseError: LocalizedError {
case sudoRequired(command: String)
case other(Error)

var errorDescription: String? {
switch self {
case let .sudoRequired(command):
return command
case let .other(error):
return String(describing: error)
}
}
}
}
49 changes: 0 additions & 49 deletions Sources/XcodeVersionManager/Models/XcodeApplication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,55 +16,6 @@ struct XcodeApplication: Encodable {
}
}

// MARK: - Use
extension XcodeApplication {
enum XcodeSelectPermissions {
case inherit
case sudoAskpass
}

func use(permissions: XcodeSelectPermissions) throws {
let xcodePath = url.absoluteURL.path

switch permissions {
case .inherit:
// Works if xcvm is run with superuser permissions
try Process.execute(
"/usr/bin/xcode-select",
arguments: [
"--switch",
xcodePath
]
)
case .sudoAskpass:
// Works if
// xcode-select has NOPASSWD set in sudoers
// touchid is enabled for sudo
// SUDO_ASKPASS environment variable is set with a helper program
// otherwise fails to prompt for password
try Process.execute(
"/usr/bin/sudo",
arguments: [
"--askpass",
"/usr/bin/xcode-select",
"--switch",
xcodePath
]
)
}

// Register Xcode so plugins work correctly.
// https://nshipster.com/xcode-source-extensions/#using-pluginkit
try Process.execute(
"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
arguments: [
"-f",
xcodePath
]
)
}
}

// MARK: - Comparable
extension XcodeApplication: Comparable {
static func < (lhs: XcodeApplication, rhs: XcodeApplication) -> Bool {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

public struct StandardErrorOutputStream: TextOutputStream {
public init() {}

public func write(_ string: String) {
let data = Data(string.utf8)
try? FileHandle.standardError.write(contentsOf: data)
}
}