Update the installer to print the errors when the commands fail

This commit is contained in:
Pedro Piñera 2020-02-25 15:12:23 +01:00
parent 1c5cb693bc
commit 062ebb3ae9
9 changed files with 105 additions and 45 deletions

View File

@ -123,33 +123,56 @@ final class Installer: Installing {
// Cloning and building
Printer.shared.print("Pulling source code")
try System.shared.run("/usr/bin/env", "git", "clone", Constants.gitRepositoryURL, temporaryDirectory.path.pathString)
_ = try System.shared.observable(["/usr/bin/env", "git", "clone", Constants.gitRepositoryURL, temporaryDirectory.path.pathString])
.mapToString()
.printStandardError()
.toBlocking()
.last()
do {
try System.shared.run("/usr/bin/env", "git", "-C", temporaryDirectory.path.pathString, "checkout", version)
} catch let error as TuistSupport.SystemError {
if error.description.contains("did not match any file(s) known to git") {
let gitCheckoutResult = System.shared.observable(["/usr/bin/env", "git", "-C", temporaryDirectory.path.pathString, "checkout", version])
.mapToString()
.toBlocking()
.materialize()
if case let .failed(elements, error) = gitCheckoutResult {
if elements.map({ $0.value }).first(where: { $0.contains("did not match any file(s) known to git") }) != nil {
throw InstallerError.versionNotFound(version)
} else {
throw error
}
throw error
}
Printer.shared.print("Building using Swift (it might take a while)")
let swiftPath = try System.shared.capture("/usr/bin/xcrun", "-f", "swift").spm_chuzzle()!
let swiftPath = try System.shared
.observable(["/usr/bin/xcrun", "-f", "swift"])
.mapToString()
.collectOutput()
.toBlocking()
.last()!
.standardOutput
.spm_chuzzle()!
try System.shared.run(swiftPath, "build",
"--product", "tuist",
"--package-path", temporaryDirectory.path.pathString,
"--configuration", "release")
_ = try System.shared.observable([swiftPath, "build",
"--product", "tuist",
"--package-path", temporaryDirectory.path.pathString,
"--configuration", "release"])
.mapToString()
.printStandardError()
.toBlocking()
.last()
try System.shared.run(swiftPath, "build",
"--product", "ProjectDescription",
"--package-path", temporaryDirectory.path.pathString,
"--configuration", "release",
"-Xswiftc", "-enable-library-evolution",
"-Xswiftc", "-emit-module-interface",
"-Xswiftc", "-emit-module-interface-path",
"-Xswiftc", temporaryDirectory.path.appending(RelativePath(".build/release/ProjectDescription.swiftinterface")).pathString) // swiftlint:disable:this line_length
_ = try System.shared.observable([swiftPath, "build",
"--product", "ProjectDescription",
"--package-path", temporaryDirectory.path.pathString,
"--configuration", "release",
"-Xswiftc", "-enable-library-evolution",
"-Xswiftc", "-emit-module-interface",
"-Xswiftc", "-emit-module-interface-path",
"-Xswiftc", temporaryDirectory.path.appending(RelativePath(".build/release/ProjectDescription.swiftinterface")).pathString]) // swiftlint:disable:this line_length
.mapToString()
.printStandardError()
.toBlocking()
.last()
if FileHandler.shared.exists(installationDirectory) {
try FileHandler.shared.delete(installationDirectory)

View File

@ -1,7 +1,7 @@
import Foundation
import RxSwift
extension Observable where Element == SystemEvent<Data> {
public extension Observable where Element == SystemEvent<Data> {
/// Returns another observable where the standard output and error data are mapped
/// to a string.
func mapToString() -> Observable<SystemEvent<String>> {
@ -9,7 +9,19 @@ extension Observable where Element == SystemEvent<Data> {
}
}
extension Observable where Element == SystemEvent<String> {
public extension Observable where Element == SystemEvent<String> {
/// Returns an observable that prints the standard error.
func printStandardError() -> Observable<SystemEvent<String>> {
self.do(onNext: { event in
switch event {
case let .standardError(error):
Printer.shared.print(errorMessage: "\(error)")
default:
return
}
})
}
/// Returns an observable that collects and merges the standard output and error into a single string.
func collectAndMergeOutput() -> Observable<String> {
reduce("") { (collected, event) -> String in

View File

@ -153,25 +153,25 @@ extension ProcessResult {
func throwIfErrored() throws {
switch exitStatus {
case let .signalled(code):
throw TuistSupport.SystemError.signalled(code: code)
throw TuistSupport.SystemError.signalled(command: arguments.first!, code: code)
case let .terminated(code):
if code != 0 {
throw TuistSupport.SystemError.terminated(code: code, error: try utf8stderrOutput())
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: code)
}
}
}
}
public enum SystemError: FatalError {
case terminated(code: Int32, error: String)
case signalled(code: Int32)
public enum SystemError: FatalError, Equatable {
case terminated(command: String, code: Int32)
case signalled(command: String, code: Int32)
public var description: String {
switch self {
case let .signalled(code):
return "Command interrupted with a signal \(code)"
case let .terminated(code, error):
return "Command exited with error code \(code) and error: \(error)"
case let .signalled(command, code):
return "The '\(command)' was interrupted with a signal \(code)"
case let .terminated(command, code):
return "The '\(command)' command exited with error code \(code)"
}
}
@ -386,18 +386,25 @@ public final class System: Systeming {
public func observable(_ arguments: [String], verbose: Bool, environment: [String: String]) -> Observable<SystemEvent<Data>> {
Observable.create { (observer) -> Disposable in
var errorData: [UInt8] = []
let process = Process(arguments: arguments,
environment: environment,
outputRedirection: .stream(stdout: { bytes in
observer.onNext(.standardOutput(Data(bytes)))
}, stderr: { bytes in
errorData.append(contentsOf: bytes)
observer.onNext(.standardError(Data(bytes)))
}),
verbose: verbose,
startNewProcessGroup: false)
do {
try process.launch()
try process.waitUntilExit().throwIfErrored()
var result = try process.waitUntilExit()
result = ProcessResult(arguments: result.arguments,
exitStatus: result.exitStatus,
output: result.output,
stderrOutput: result.stderrOutput.map { _ in errorData })
try result.throwIfErrored()
observer.onCompleted()
} catch {
observer.onError(error)

View File

@ -1,9 +1,9 @@
import Foundation
struct SystemCollectedOutput {
public struct SystemCollectedOutput {
/// Standard output.
var standardOutput: String = ""
public var standardOutput: String = ""
/// Standard error.
var standardError: String = ""
public var standardError: String = ""
}

View File

@ -1,3 +1,4 @@
// https://github.com/rhodgkins/SwiftHTTPStatusCodes
//
// HTTPStatusCodes+Extensions.swift
// HTTPStatusCodes

View File

@ -33,10 +33,10 @@ public final class MockSystem: Systeming {
public func run(_ arguments: [String]) throws {
let command = arguments.joined(separator: " ")
guard let stub = stubs[command] else {
throw TuistSupport.SystemError.terminated(code: 1, error: "command '\(command)' not stubbed")
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: 1)
}
if stub.exitstatus != 0 {
throw TuistSupport.SystemError.terminated(code: 1, error: stub.stderror ?? "")
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: 1)
}
}
@ -59,10 +59,10 @@ public final class MockSystem: Systeming {
public func capture(_ arguments: [String], verbose _: Bool, environment _: [String: String]) throws -> String {
let command = arguments.joined(separator: " ")
guard let stub = stubs[command] else {
throw TuistSupport.SystemError.terminated(code: 1, error: "command '\(command)' not stubbed")
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: 1)
}
if stub.exitstatus != 0 {
throw TuistSupport.SystemError.terminated(code: 1, error: stub.stderror ?? "")
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: 1)
}
return stub.stdout ?? ""
}
@ -91,13 +91,13 @@ public final class MockSystem: Systeming {
) throws {
let command = arguments.joined(separator: " ")
guard let stub = stubs[command] else {
throw TuistSupport.SystemError.terminated(code: 1, error: "command '\(command)' not stubbed")
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: 1)
}
if stub.exitstatus != 0 {
if let error = stub.stderror {
redirection.outputClosures?.stderrClosure([UInt8](error.data(using: .utf8)!))
}
throw TuistSupport.SystemError.terminated(code: 1, error: stub.stderror ?? "")
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: 1)
}
}
@ -113,14 +113,14 @@ public final class MockSystem: Systeming {
Observable.create { (observer) -> Disposable in
let command = arguments.joined(separator: " ")
guard let stub = self.stubs[command] else {
observer.onError(TuistSupport.SystemError.terminated(code: 1, error: "command '\(command)' not stubbed"))
observer.onError(TuistSupport.SystemError.terminated(command: arguments.first!, code: 1))
return Disposables.create()
}
guard stub.exitstatus == 0 else {
if let error = stub.stderror {
observer.onNext(.standardError(error.data(using: .utf8)!))
}
observer.onError(TuistSupport.SystemError.terminated(code: 1, error: stub.stderror ?? ""))
observer.onError(TuistSupport.SystemError.terminated(command: arguments.first!, code: 1))
return Disposables.create()
}
if let stdout = stub.stdout {
@ -138,10 +138,10 @@ public final class MockSystem: Systeming {
public func async(_ arguments: [String], verbose _: Bool, environment _: [String: String]) throws {
let command = arguments.joined(separator: " ")
guard let stub = stubs[command] else {
throw TuistSupport.SystemError.terminated(code: 1, error: "command '\(command)' not stubbed")
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: 1)
}
if stub.exitstatus != 0 {
throw TuistSupport.SystemError.terminated(code: 1, error: stub.stderror ?? "")
throw TuistSupport.SystemError.terminated(command: arguments.first!, code: 1)
}
}

View File

@ -117,7 +117,7 @@ final class GoogleCloudStorageClientTests: TuistUnitTestCase {
var releaseRequest = URLRequest(url: releaseURL)
releaseRequest.httpMethod = "HEAD"
let buildURL = GoogleCloudStorageClient.url(buildsPath: "tuist-\(version).zip")
let buildURL = GoogleCloudStorageClient.url(buildsPath: "\(version).zip")
var buildRequest = URLRequest(url: buildURL)
buildRequest.httpMethod = "HEAD"

View File

@ -48,6 +48,23 @@ final class SystemIntegrationTests: TuistTestCase {
}
}
func test_observable_when_it_errors() throws {
// Given
let observable = subject.observable(["/usr/bin/xcrun", "invalid"]).mapToString()
// When
let result = observable.toBlocking().materialize()
// Then
switch result {
case .completed:
XCTFail("expected command to fail but it did not")
case let .failed(elements, error):
XCTAssertTrue(elements.first(where: { $0.value.contains("errno=No such file or directory") }) != nil)
XCTAssertEqual(error as? TuistSupport.SystemError, TuistSupport.SystemError.terminated(command: "/usr/bin/xcrun", code: 72))
}
}
func test_pass_DEVELOPER_DIR() throws {
try sandbox("DEVELOPER_DIR", value: "/Applications/Xcode/Xcode-10.2.1.app/Contents/Developer/") {
let result = try subject.capture("env")