Skip to content

Commit b760467

Browse files
committed
[Commands] Redirect output to stderr when using swift-run
rdar://problem/34059859 SR-7823
1 parent 32b8d90 commit b760467

File tree

5 files changed

+127
-51
lines changed

5 files changed

+127
-51
lines changed

Sources/Commands/Error.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,15 @@ func print(error: Any) {
7575
writer.write("\n")
7676
}
7777

78-
func print(diagnostic: Diagnostic) {
78+
func print(diagnostic: Diagnostic, stdoutStream: OutputByteStream) {
7979

80-
let writer = diagnostic.behavior == .note ? InteractiveWriter.stdout : InteractiveWriter.stderr
80+
let writer: InteractiveWriter
81+
82+
if diagnostic.behavior == .note {
83+
writer = InteractiveWriter(stream: stdoutStream)
84+
} else {
85+
writer = InteractiveWriter.stderr
86+
}
8187

8288
if !(diagnostic.location is UnknownLocation) {
8389
writer.write(diagnostic.location.localizedDescription)

Sources/Commands/SwiftRunTool.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,18 @@ public class SwiftRunTool: SwiftTool<RunToolOptions> {
102102
case .run:
103103
// Detect deprecated uses of swift run to interpret scripts.
104104
if let executable = options.executable, isValidSwiftFilePath(executable) {
105-
print(diagnostic: Diagnostic(
106-
location: UnknownLocation.location,
107-
data: RunFileDeprecatedDiagnostic()))
105+
diagnostics.emit(data: RunFileDeprecatedDiagnostic())
108106
// Redirect execution to the toolchain's swift executable.
109107
let swiftInterpreterPath = try getToolchain().swiftInterpreter
110108
// Prepend the script to interpret to the arguments.
111109
let arguments = [executable] + options.arguments
112110
try run(swiftInterpreterPath, arguments: arguments)
113111
return
114112
}
113+
114+
// Redirect stdout to stderr because swift-run clients usually want
115+
// to ignore swiftpm's output and only care about the tool's output.
116+
self.redirectStdoutToStderr()
115117

116118
let plan = try BuildPlan(buildParameters: self.buildParameters(), graph: loadPackageGraph(), diagnostics: diagnostics)
117119
let product = try findProduct(in: plan.graph)

Sources/Commands/SwiftTool.swift

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ struct TargetNotFoundDiagnostic: DiagnosticData {
6969

7070
private class ToolWorkspaceDelegate: WorkspaceDelegate {
7171

72+
/// The stream to use for reporting progress.
73+
private let stdoutStream: OutputByteStream
74+
75+
init(_ stdoutStream: OutputByteStream) {
76+
self.stdoutStream = stdoutStream
77+
}
78+
7279
func packageGraphWillLoad(
7380
currentGraph: PackageGraph,
7481
dependencies: AnySequence<ManagedDependency>,
@@ -77,39 +84,52 @@ private class ToolWorkspaceDelegate: WorkspaceDelegate {
7784
}
7885

7986
func fetchingWillBegin(repository: String) {
80-
print("Fetching \(repository)")
87+
stdoutStream <<< "Fetching \(repository)"
88+
stdoutStream <<< "\n"
89+
stdoutStream.flush()
8190
}
8291

8392
func fetchingDidFinish(repository: String, diagnostic: Diagnostic?) {
8493
}
8594

8695
func repositoryWillUpdate(_ repository: String) {
87-
print("Updating \(repository)")
96+
stdoutStream <<< "Updating \(repository)"
97+
stdoutStream <<< "\n"
98+
stdoutStream.flush()
8899
}
89100

90101
func repositoryDidUpdate(_ repository: String) {
91102
}
92103

93104
func dependenciesUpToDate() {
94-
print("Everything is already up-to-date")
105+
stdoutStream <<< "Everything is already up-to-date"
106+
stdoutStream <<< "\n"
107+
stdoutStream.flush()
95108
}
96109

97110
func cloning(repository: String) {
98-
print("Cloning \(repository)")
111+
stdoutStream <<< "Cloning \(repository)"
112+
stdoutStream <<< "\n"
113+
stdoutStream.flush()
99114
}
100115

101116
func checkingOut(repository: String, atReference reference: String, to path: AbsolutePath) {
102-
// FIXME: This is temporary output similar to old one, we will need to figure
103-
// out better reporting text.
104-
print("Resolving \(repository) at \(reference)")
117+
stdoutStream <<< "Resolving \(repository) at \(reference)"
118+
stdoutStream <<< "\n"
119+
stdoutStream.flush()
105120
}
106121

107122
func removing(repository: String) {
108-
print("Removing \(repository)")
123+
stdoutStream <<< "Removing \(repository)"
124+
stdoutStream <<< "\n"
125+
stdoutStream.flush()
109126
}
110127

111128
func warning(message: String) {
112-
print("warning: " + message)
129+
// FIXME: We should emit warnings through the diagnostic engine.
130+
stdoutStream <<< "warning: " <<< message
131+
stdoutStream <<< "\n"
132+
stdoutStream.flush()
113133
}
114134

115135
func managedDependenciesDidUpdate(_ dependencies: AnySequence<ManagedDependency>) {
@@ -165,6 +185,22 @@ final class BuildManifestRegenerationToken {
165185
}
166186
}
167187

188+
/// Handler for the main DiagnosticsEngine used by the SwiftTool class.
189+
private final class DiagnosticsEngineHandler {
190+
191+
/// The standard output stream.
192+
var stdoutStream = Basic.stdoutStream
193+
194+
/// The default instance.
195+
static let `default` = DiagnosticsEngineHandler()
196+
197+
private init() {}
198+
199+
func diagnosticsHandler(_ diagnostic: Diagnostic) {
200+
print(diagnostic: diagnostic, stdoutStream: stderrStream)
201+
}
202+
}
203+
168204
public class SwiftTool<Options: ToolOptions> {
169205
/// The original working directory.
170206
let originalWorkingDirectory: AbsolutePath
@@ -202,11 +238,19 @@ public class SwiftTool<Options: ToolOptions> {
202238
let interruptHandler: InterruptHandler
203239

204240
/// The diagnostics engine.
205-
let diagnostics = DiagnosticsEngine(handlers: [SwiftTool.diagnosticsHandler])
241+
let diagnostics: DiagnosticsEngine = DiagnosticsEngine(
242+
handlers: [DiagnosticsEngineHandler.default.diagnosticsHandler])
206243

207244
/// The execution status of the tool.
208245
var executionStatus: ExecutionStatus = .success
209246

247+
/// The stream to print standard output on.
248+
fileprivate var stdoutStream: OutputByteStream = Basic.stdoutStream
249+
250+
/// If true, Redirects the stdout stream to stderr when invoking
251+
/// `swift-build-tool`.
252+
private var shouldRedirectStdoutToStderr = false
253+
210254
/// Create an instance of this tool.
211255
///
212256
/// - parameter args: The command line arguments to be passed to this tool.
@@ -394,7 +438,7 @@ public class SwiftTool<Options: ToolOptions> {
394438
if let workspace = _workspace {
395439
return workspace
396440
}
397-
let delegate = ToolWorkspaceDelegate()
441+
let delegate = ToolWorkspaceDelegate(self.stdoutStream)
398442
let rootPackage = try getPackageRoot()
399443
let provider = GitRepositoryProvider(processSet: processSet)
400444
let workspace = Workspace(
@@ -431,10 +475,6 @@ public class SwiftTool<Options: ToolOptions> {
431475
SwiftTool.exit(with: executionStatus)
432476
}
433477

434-
static func diagnosticsHandler(_ diagnostic: Diagnostic) {
435-
print(diagnostic: diagnostic)
436-
}
437-
438478
/// Exit the tool with the given execution status.
439479
private static func exit(with status: ExecutionStatus) -> Never {
440480
switch status {
@@ -448,6 +488,13 @@ public class SwiftTool<Options: ToolOptions> {
448488
fatalError("Must be implemented by subclasses")
449489
}
450490

491+
/// Start redirecting the standard output stream to the standard error stream.
492+
func redirectStdoutToStderr() {
493+
self.shouldRedirectStdoutToStderr = true
494+
self.stdoutStream = Basic.stderrStream
495+
DiagnosticsEngineHandler.default.stdoutStream = Basic.stdoutStream
496+
}
497+
451498
/// Resolve the dependencies.
452499
func resolve() throws {
453500
let workspace = try getActiveWorkspace()
@@ -601,11 +648,19 @@ public class SwiftTool<Options: ToolOptions> {
601648
env["TMPDIR"] = tempDir.asString
602649

603650
// Run llbuild and print output on standard streams.
604-
let process = Process(arguments: args, environment: env, redirectOutput: false)
651+
let process = Process(arguments: args, environment: env, redirectOutput: shouldRedirectStdoutToStderr)
605652
try process.launch()
606653
try processSet.add(process)
607654
let result = try process.waitUntilExit()
608655

656+
// Emit the output to the selected stream if we need to redirect the
657+
// stream.
658+
if shouldRedirectStdoutToStderr {
659+
self.stdoutStream <<< (try result.utf8stderrOutput())
660+
self.stdoutStream <<< (try result.utf8Output())
661+
self.stdoutStream.flush()
662+
}
663+
609664
guard result.exitStatus == .terminated(code: 0) else {
610665
throw ProcessResult.Error.nonZeroExit(result)
611666
}

Sources/TestSupport/SwiftPMProduct.swift

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,31 @@ public enum SwiftPMProduct {
8282
env: [String: String]? = nil,
8383
printIfError: Bool = false
8484
) throws -> String {
85+
86+
let result = try executeProcess(
87+
args, packagePath: packagePath,
88+
env: env)
89+
90+
let output = try result.utf8Output()
91+
let stderr = try result.utf8stderrOutput()
92+
93+
if result.exitStatus == .terminated(code: 0) {
94+
// FIXME: We should return stderr separately.
95+
return output + stderr
96+
}
97+
throw SwiftPMProductError.executionFailure(
98+
error: ProcessResult.Error.nonZeroExit(result),
99+
output: output,
100+
stderr: stderr
101+
)
102+
}
103+
104+
public func executeProcess(
105+
_ args: [String],
106+
packagePath: AbsolutePath? = nil,
107+
env: [String: String]? = nil
108+
) throws -> ProcessResult {
109+
85110
var environment = ProcessInfo.processInfo.environment
86111
for (key, value) in (env ?? [:]) {
87112
environment[key] = value
@@ -102,25 +127,7 @@ public enum SwiftPMProduct {
102127
}
103128
completeArgs += args
104129

105-
let result = try Process.popen(arguments: completeArgs, environment: environment)
106-
let output = try result.utf8Output()
107-
let stderr = try result.utf8stderrOutput()
108-
109-
if result.exitStatus == .terminated(code: 0) {
110-
// FIXME: We should return stderr separately.
111-
return output + stderr
112-
}
113-
if printIfError {
114-
print("**** FAILURE EXECUTING SUBPROCESS ****")
115-
print("command: " + completeArgs.map({ $0.shellEscaped() }).joined(separator: " "))
116-
print("SWIFT_EXEC:", environment["SWIFT_EXEC"] ?? "nil")
117-
print("output:", output)
118-
}
119-
throw SwiftPMProductError.executionFailure(
120-
error: ProcessResult.Error.nonZeroExit(result),
121-
output: output,
122-
stderr: stderr
123-
)
130+
return try Process.popen(arguments: completeArgs, environment: environment)
124131
}
125132

126133
public static func packagePath(for packageName: String, packageRoot: AbsolutePath) throws -> AbsolutePath {

Tests/CommandsTests/RunToolTests.swift

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,25 @@ final class RunToolTests: XCTestCase {
3434

3535
func testUnkownProductAndArgumentPassing() throws {
3636
fixture(name: "Miscellaneous/EchoExecutable") { path in
37+
38+
let result = try SwiftPMProduct.SwiftRun.executeProcess(
39+
["secho", "1", "--hello", "world"], packagePath: path)
40+
41+
// We only expect tool's output on the stdout stream.
42+
XCTAssertMatch(try result.utf8Output(), .contains("""
43+
"1" "--hello" "world"
44+
"""))
45+
46+
// swift-build-tool output should go to stderr.
47+
XCTAssertMatch(try result.utf8stderrOutput(), .contains("Compile Swift Module"))
48+
XCTAssertMatch(try result.utf8stderrOutput(), .contains("Linking"))
49+
3750
do {
3851
_ = try execute(["unknown"], packagePath: path)
3952
XCTFail("Unexpected success")
4053
} catch SwiftPMProductError.executionFailure(_, _, let stderr) {
4154
XCTAssertEqual(stderr, "error: no executable product named 'unknown'\n")
4255
}
43-
44-
let runOutput = try execute(["secho", "1", "--hello", "world"], packagePath: path)
45-
let outputLines = runOutput.split(separator: "\n")
46-
XCTAssertEqual(String(outputLines.last!), """
47-
"\(getcwd())" "1" "--hello" "world"
48-
""")
4956
}
5057
}
5158

@@ -59,19 +66,18 @@ final class RunToolTests: XCTestCase {
5966
}
6067

6168
var runOutput = try execute(["exec1"], packagePath: path)
62-
var outputLines = runOutput.split(separator: "\n")
63-
XCTAssertEqual(outputLines.last!, "1")
69+
XCTAssertMatch(runOutput, .contains("1"))
70+
6471
runOutput = try execute(["exec2"], packagePath: path)
65-
outputLines = runOutput.split(separator: "\n")
66-
XCTAssertEqual(outputLines.last!, "2")
72+
XCTAssertMatch(runOutput, .contains("2"))
6773
}
6874
}
6975

7076
func testUnreachableExecutable() throws {
7177
fixture(name: "Miscellaneous/UnreachableTargets") { path in
7278
let output = try execute(["bexec"], packagePath: path.appending(component: "A"))
7379
let outputLines = output.split(separator: "\n")
74-
XCTAssertEqual(outputLines.last!, "BTarget2")
80+
XCTAssertEqual(outputLines.first, "BTarget2")
7581
}
7682
}
7783

0 commit comments

Comments
 (0)