Skip to content

Commit

Permalink
Provide better error messages for underlying shell commands
Browse files Browse the repository at this point in the history
When shell commands failed a helpful error message wasn't always provided to the user. This change improves the specific XcodeInstaller.Error cases and their error messages. Not all Current.shell command failures are caught and turned into our own error type, so this also improves the fallback behaviour by printing output from Process.PMKError.execution, which is omitted by default.
  • Loading branch information
interstateone committed Oct 3, 2019
1 parent 66dff74 commit 22dc57c
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 13 deletions.
49 changes: 43 additions & 6 deletions Sources/XcodesKit/XcodeInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,42 @@ public final class XcodeInstaller {
static let XcodeTeamIdentifier = "59GAB85EFG"
static let XcodeCertificateAuthority = ["Software Signing", "Apple Code Signing Certification Authority", "Apple Root CA"]

public enum Error: Swift.Error, Equatable {
public enum Error: LocalizedError, Equatable {
case failedToMoveXcodeToApplications
case failedSecurityAssessment(xcode: InstalledXcode, output: String)
case codesignVerifyFailed
case codesignVerifyFailed(output: String)
case unexpectedCodeSigningIdentity(identifier: String, certificateAuthority: [String])
case unsupportedFileFormat(extension: String)

public var errorDescription: String? {
switch self {
case .failedToMoveXcodeToApplications:
return "Failed to move Xcode to the /Applications directory."
case .failedSecurityAssessment(let xcode, let output):
return """
Xcode \(xcode.version) failed its security assessment with the following output:
\(output)
It remains installed at \(xcode.path) if you wish to use it anyways.
"""
case .codesignVerifyFailed(let output):
return """
The downloaded Xcode failed code signing verification with the following output:
\(output)
"""
case .unexpectedCodeSigningIdentity(let identity, let certificateAuthority):
return """
The downloaded Xcode doesn't have the expected code signing identity.
Got:
\(identity)
\(certificateAuthority)
Expected:
\(XcodeInstaller.XcodeTeamIdentifier)
\(XcodeInstaller.XcodeCertificateAuthority)
"""
case .unsupportedFileFormat(let fileExtension):
return "xcodes doesn't (yet) support installing Xcode from the \(fileExtension) file format."
}
}
}

public init() {}
Expand Down Expand Up @@ -51,7 +82,7 @@ public final class XcodeInstaller {
let destinationURL = Path.root.join("Applications").join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url
switch archiveURL.pathExtension {
case "xip":
return try unarchiveAndMoveXIP(at: archiveURL, to: destinationURL).map { xcodeURL in
return unarchiveAndMoveXIP(at: archiveURL, to: destinationURL).map { xcodeURL in
guard
let path = Path(url: xcodeURL),
Current.files.fileExists(atPath: path.string),
Expand Down Expand Up @@ -90,7 +121,7 @@ public final class XcodeInstaller {
}
}

func unarchiveAndMoveXIP(at source: URL, to destination: URL) throws -> Promise<URL> {
func unarchiveAndMoveXIP(at source: URL, to destination: URL) -> Promise<URL> {
return firstly { () -> Promise<ProcessOutput> in
return Current.shell.unxip(source)
}
Expand Down Expand Up @@ -122,7 +153,13 @@ public final class XcodeInstaller {

func verifySigningCertificate(of url: URL) -> Promise<Void> {
return Current.shell.codesignVerify(url)
.recover { _ -> Promise<ProcessOutput> in throw Error.codesignVerifyFailed }
.recover { error -> Promise<ProcessOutput> in
var output = ""
if case let Process.PMKError.execution(_, possibleOutput, possibleError) = error {
output = [possibleOutput, possibleError].compactMap { $0 }.joined(separator: "\n")
}
throw Error.codesignVerifyFailed(output: output)
}
.map { output -> CertificateInfo in
// codesign prints to stderr
return self.parseCertificateInfo(output.err)
Expand All @@ -131,7 +168,7 @@ public final class XcodeInstaller {
guard
cert.teamIdentifier == XcodeInstaller.XcodeTeamIdentifier,
cert.authority == XcodeInstaller.XcodeCertificateAuthority
else { throw Error.codesignVerifyFailed }
else { throw Error.unexpectedCodeSigningIdentity(identifier: cert.teamIdentifier, certificateAuthority: cert.authority) }
}
}

Expand Down
9 changes: 4 additions & 5 deletions Sources/xcodes/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ try? configuration.load()
let xcodesUsername = "XCODES_USERNAME"
let xcodesPassword = "XCODES_PASSWORD"

enum XcodesError: Swift.Error, LocalizedError {
enum XcodesError: LocalizedError {
case missingUsernameOrPassword
case missingSudoerPassword
case invalidVersion(String)
Expand Down Expand Up @@ -271,11 +271,10 @@ let install = Command(usage: "install <version>", flags: [urlFlag]) { flags, arg
}
.catch { error in
switch error {
case XcodeInstaller.Error.failedSecurityAssessment(let xcode, let output):
case Process.PMKError.execution(let process, let standardOutput, let standardError):
print("""
Xcode \(xcode.version) failed its security assessment with the following output:
\(output)
It remains installed at \(xcode.path) if you wish to use it anyways.
Failed executing: `\(process)` (\(process.terminationStatus))
\([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n"))
""")
default:
print(error.legibleLocalizedDescription)
Expand Down
4 changes: 2 additions & 2 deletions Tests/XcodesKitTests/XcodesKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ final class XcodesKitTests: XCTestCase {

let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock")
installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), archiveTrashed: { _ in }, passwordInput: { Promise.value("") })
.catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.codesignVerifyFailed) }
.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")
installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip"), archiveTrashed: { _ in }, passwordInput: { Promise.value("") })
.catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.codesignVerifyFailed) }
.catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.unexpectedCodeSigningIdentity(identifier: "", certificateAuthority: [])) }
}

func test_InstallArchivedXcode_TrashesXIPWhenFinished() {
Expand Down

0 comments on commit 22dc57c

Please sign in to comment.