From 062ebb3ae939f5b4e05391f67cae2db6d7a23cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pin=CC=83era?= Date: Tue, 25 Feb 2020 15:12:23 +0100 Subject: [PATCH] Update the installer to print the errors when the commands fail --- Sources/TuistEnvKit/Installer/Installer.swift | 61 +++++++++++++------ .../System/Observable+System.swift | 16 ++++- Sources/TuistSupport/System/System.swift | 27 +++++--- .../System/SystemCollectedOutput.swift | 6 +- .../HTTPStatusCode+Extensions.swift | 1 + .../HTTPStatusCodes}/HTTPStatusCodes.swift | 0 .../Utils/MockSystem.swift | 20 +++--- .../GoogleCloudStorageClientTests.swift | 2 +- .../System/SystemIntegrationTests.swift | 17 ++++++ 9 files changed, 105 insertions(+), 45 deletions(-) rename Sources/TuistSupport/{Utils => Vendored/HTTPStatusCodes}/HTTPStatusCode+Extensions.swift (99%) rename Sources/TuistSupport/{Utils => Vendored/HTTPStatusCodes}/HTTPStatusCodes.swift (100%) diff --git a/Sources/TuistEnvKit/Installer/Installer.swift b/Sources/TuistEnvKit/Installer/Installer.swift index aa9ec15ea..f31f74545 100644 --- a/Sources/TuistEnvKit/Installer/Installer.swift +++ b/Sources/TuistEnvKit/Installer/Installer.swift @@ -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) diff --git a/Sources/TuistSupport/System/Observable+System.swift b/Sources/TuistSupport/System/Observable+System.swift index 7c868d19f..a9a37dbe2 100644 --- a/Sources/TuistSupport/System/Observable+System.swift +++ b/Sources/TuistSupport/System/Observable+System.swift @@ -1,7 +1,7 @@ import Foundation import RxSwift -extension Observable where Element == SystemEvent { +public extension Observable where Element == SystemEvent { /// Returns another observable where the standard output and error data are mapped /// to a string. func mapToString() -> Observable> { @@ -9,7 +9,19 @@ extension Observable where Element == SystemEvent { } } -extension Observable where Element == SystemEvent { +public extension Observable where Element == SystemEvent { + /// Returns an observable that prints the standard error. + func printStandardError() -> Observable> { + 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 { reduce("") { (collected, event) -> String in diff --git a/Sources/TuistSupport/System/System.swift b/Sources/TuistSupport/System/System.swift index 40f81a666..45c6da15b 100644 --- a/Sources/TuistSupport/System/System.swift +++ b/Sources/TuistSupport/System/System.swift @@ -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> { 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) diff --git a/Sources/TuistSupport/System/SystemCollectedOutput.swift b/Sources/TuistSupport/System/SystemCollectedOutput.swift index ed1e6ce43..17a147373 100644 --- a/Sources/TuistSupport/System/SystemCollectedOutput.swift +++ b/Sources/TuistSupport/System/SystemCollectedOutput.swift @@ -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 = "" } diff --git a/Sources/TuistSupport/Utils/HTTPStatusCode+Extensions.swift b/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCode+Extensions.swift similarity index 99% rename from Sources/TuistSupport/Utils/HTTPStatusCode+Extensions.swift rename to Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCode+Extensions.swift index fb120f61a..e7626be80 100644 --- a/Sources/TuistSupport/Utils/HTTPStatusCode+Extensions.swift +++ b/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCode+Extensions.swift @@ -1,3 +1,4 @@ +// https://github.com/rhodgkins/SwiftHTTPStatusCodes // // HTTPStatusCodes+Extensions.swift // HTTPStatusCodes diff --git a/Sources/TuistSupport/Utils/HTTPStatusCodes.swift b/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCodes.swift similarity index 100% rename from Sources/TuistSupport/Utils/HTTPStatusCodes.swift rename to Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCodes.swift diff --git a/Sources/TuistSupportTesting/Utils/MockSystem.swift b/Sources/TuistSupportTesting/Utils/MockSystem.swift index 93f94232c..702608f57 100644 --- a/Sources/TuistSupportTesting/Utils/MockSystem.swift +++ b/Sources/TuistSupportTesting/Utils/MockSystem.swift @@ -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) } } diff --git a/Tests/TuistEnvKitTests/GoogleCloudStorage/GoogleCloudStorageClientTests.swift b/Tests/TuistEnvKitTests/GoogleCloudStorage/GoogleCloudStorageClientTests.swift index d92ace426..d9b7b7212 100644 --- a/Tests/TuistEnvKitTests/GoogleCloudStorage/GoogleCloudStorageClientTests.swift +++ b/Tests/TuistEnvKitTests/GoogleCloudStorage/GoogleCloudStorageClientTests.swift @@ -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" diff --git a/Tests/TuistSupportIntegrationTests/System/SystemIntegrationTests.swift b/Tests/TuistSupportIntegrationTests/System/SystemIntegrationTests.swift index 1e59c652a..b8e0a5db8 100644 --- a/Tests/TuistSupportIntegrationTests/System/SystemIntegrationTests.swift +++ b/Tests/TuistSupportIntegrationTests/System/SystemIntegrationTests.swift @@ -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")