Skip to content

Commit

Permalink
Merge pull request #76 from interstateone/65-output-command-errors
Browse files Browse the repository at this point in the history
Improve output for errors from underlying shell commands
  • Loading branch information
interstateone authored Oct 3, 2019
2 parents 66dff74 + 3a94285 commit b8ba06a
Show file tree
Hide file tree
Showing 3 changed files with 55 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
10 changes: 8 additions & 2 deletions Tests/XcodesKitTests/XcodesKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ final class XcodesKitTests: XCTestCase {
XCTAssertEqual(value, Path.applicationSupport.join("com.robotsandpencils.xcodes").join("Xcode-0.0.0.xip").url)
XCTAssertNil(xcodeDownloadURL)
}
.cauterize()
}

func test_DownloadOrUseExistingArchive_DownloadsArchive() {
Expand All @@ -73,6 +74,7 @@ final class XcodesKitTests: XCTestCase {
XCTAssertEqual(value, Path.applicationSupport.join("com.robotsandpencils.xcodes").join("Xcode-0.0.0.xip").url)
XCTAssertEqual(xcodeDownloadURL, URL(string: "https://apple.com/xcode.xip")!)
}
.cauterize()
}

func test_InstallArchivedXcode_SecurityAssessmentFails_Throws() {
Expand All @@ -89,15 +91,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 All @@ -111,6 +113,7 @@ final class XcodesKitTests: XCTestCase {
let xipURL = URL(fileURLWithPath: "/Xcode-0.0.0.xip")
installer.installArchivedXcode(xcode, at: xipURL, archiveTrashed: { _ in }, passwordInput: { Promise.value("") })
.ensure { XCTAssertEqual(trashedItemAtURL, xipURL) }
.cauterize()
}

func test_UninstallXcode_TrashesXcode() {
Expand All @@ -123,6 +126,7 @@ final class XcodesKitTests: XCTestCase {
let installedXcode = InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!
installer.uninstallXcode(installedXcode)
.ensure { XCTAssertEqual(trashedItemAtURL, installedXcode.path.url) }
.cauterize()
}

func test_VerifySecurityAssessment_Fails() {
Expand All @@ -131,6 +135,7 @@ final class XcodesKitTests: XCTestCase {
let installedXcode = InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!
installer.verifySecurityAssessment(of: installedXcode)
.tap { result in XCTAssertFalse(result.isFulfilled) }
.cauterize()
}

func test_VerifySecurityAssessment_Succeeds() {
Expand All @@ -139,6 +144,7 @@ final class XcodesKitTests: XCTestCase {
let installedXcode = InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!
installer.verifySecurityAssessment(of: installedXcode)
.tap { result in XCTAssertTrue(result.isFulfilled) }
.cauterize()
}

func test_MigrateApplicationSupport_NoSupportFiles() {
Expand Down

0 comments on commit b8ba06a

Please sign in to comment.