diff options
author | Tobias Bosch <tbosch@google.com> | 2019-08-16 11:47:00 -0700 |
---|---|---|
committer | Tobias Bosch <tbosch@google.com> | 2019-08-20 17:48:54 +0000 |
commit | a50a9c16b6a3b2522bea3b6562d023a7b87dbd49 (patch) | |
tree | 4d17d8e02962ba4e62c66e49f520cb1b310adc7d | |
parent | 36c19215f13c983c2ae0096ed9170fd064744622 (diff) | |
download | toolchain-utils-a50a9c16b6a3b2522bea3b6562d023a7b87dbd49.tar.gz |
Forward os.Stdin to child processes.
BUG=chromium:773875
TEST=unit tests, build glibc with compiler wrapper
Change-Id: I0b5c1f5adaee18499b72747cd6042b00a9d52c1d
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/1760973
Reviewed-by: George Burgess <gbiv@chromium.org>
Tested-by: Tobias Bosch <tbosch@google.com>
-rw-r--r-- | compiler_wrapper/bisect_flag_test.go | 4 | ||||
-rw-r--r-- | compiler_wrapper/clang_flags.go | 2 | ||||
-rw-r--r-- | compiler_wrapper/clang_flags_test.go | 4 | ||||
-rw-r--r-- | compiler_wrapper/clang_syntax_flag.go | 18 | ||||
-rw-r--r-- | compiler_wrapper/clang_syntax_flag_test.go | 32 | ||||
-rw-r--r-- | compiler_wrapper/clang_tidy_flag.go | 4 | ||||
-rw-r--r-- | compiler_wrapper/clang_tidy_flag_test.go | 16 | ||||
-rw-r--r-- | compiler_wrapper/compiler_wrapper.go | 13 | ||||
-rw-r--r-- | compiler_wrapper/compiler_wrapper_test.go | 20 | ||||
-rw-r--r-- | compiler_wrapper/disable_werror_flag.go | 6 | ||||
-rw-r--r-- | compiler_wrapper/disable_werror_flag_test.go | 43 | ||||
-rw-r--r-- | compiler_wrapper/env.go | 45 | ||||
-rw-r--r-- | compiler_wrapper/goldenutil_test.go | 2 | ||||
-rw-r--r-- | compiler_wrapper/oldwrapper.go | 14 | ||||
-rw-r--r-- | compiler_wrapper/oldwrapper_test.go | 40 | ||||
-rw-r--r-- | compiler_wrapper/rusage_flag.go | 2 | ||||
-rw-r--r-- | compiler_wrapper/rusage_flag_test.go | 21 | ||||
-rw-r--r-- | compiler_wrapper/testutil_test.go | 24 |
18 files changed, 238 insertions, 72 deletions
diff --git a/compiler_wrapper/bisect_flag_test.go b/compiler_wrapper/bisect_flag_test.go index 15021327..11ab2dcf 100644 --- a/compiler_wrapper/bisect_flag_test.go +++ b/compiler_wrapper/bisect_flag_test.go @@ -62,7 +62,7 @@ func TestDefaultBisectDir(t *testing.T) { func TestForwardStdOutAndStdErrAndExitCodeFromBisect(t *testing.T) { withBisectTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { fmt.Fprint(stdout, "somemessage") fmt.Fprint(stderr, "someerror") return newExitCodeError(23) @@ -82,7 +82,7 @@ func TestForwardStdOutAndStdErrAndExitCodeFromBisect(t *testing.T) { func TestForwardGeneralErrorFromBisect(t *testing.T) { withBisectTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { return errors.New("someerror") } stderr := ctx.mustFail(callCompiler(ctx, ctx.cfg, diff --git a/compiler_wrapper/clang_flags.go b/compiler_wrapper/clang_flags.go index 0cb10c7e..30a0b576 100644 --- a/compiler_wrapper/clang_flags.go +++ b/compiler_wrapper/clang_flags.go @@ -167,7 +167,7 @@ func getClangResourceDir(env env, clangPath string) (string, error) { Args: []string{"--print-resource-dir"}, } stdoutBuffer := bytes.Buffer{} - if err := env.run(readResourceCmd, &stdoutBuffer, env.stderr()); err != nil { + if err := env.run(readResourceCmd, nil, &stdoutBuffer, env.stderr()); err != nil { return "", wrapErrorwithSourceLocf(err, "failed to call clang to read the resouce-dir: %#v", readResourceCmd) diff --git a/compiler_wrapper/clang_flags_test.go b/compiler_wrapper/clang_flags_test.go index 5fe13c31..1d651194 100644 --- a/compiler_wrapper/clang_flags_test.go +++ b/compiler_wrapper/clang_flags_test.go @@ -94,7 +94,7 @@ func TestClangPathForClangHostWrapper(t *testing.T) { func TestUseXclangPathAndCalcResourceDirByNestedClangCall(t *testing.T) { withTestContext(t, func(ctx *testContext) { ctx.cfg.rootRelPath = "somepath" - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount > 1 { return nil } @@ -121,7 +121,7 @@ func TestUseXclangPathAndCalcResourceDirByNestedClangCall(t *testing.T) { func TestXclangPathFailIfNestedClangCallFails(t *testing.T) { withTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { fmt.Fprint(stderr, "someclangerror") return errors.New("someerror") } diff --git a/compiler_wrapper/clang_syntax_flag.go b/compiler_wrapper/clang_syntax_flag.go index e305e579..8f38c613 100644 --- a/compiler_wrapper/clang_syntax_flag.go +++ b/compiler_wrapper/clang_syntax_flag.go @@ -4,6 +4,11 @@ package main +import ( + "bytes" + "io" +) + func processClangSyntaxFlag(builder *commandBuilder) (clangSyntax bool) { builder.transformArgs(func(arg builderArg) string { if arg.value == "-clang-syntax" { @@ -15,12 +20,19 @@ func processClangSyntaxFlag(builder *commandBuilder) (clangSyntax bool) { return clangSyntax } -func checkClangSyntax(env env, clangCmd *command) (exitCode int, err error) { +func checkClangSyntax(env env, clangCmd *command, gccCmd *command) (exitCode int, err error) { clangSyntaxCmd := &command{ Path: clangCmd.Path, Args: append(clangCmd.Args, "-fsyntax-only", "-stdlib=libstdc++"), EnvUpdates: clangCmd.EnvUpdates, } - return wrapSubprocessErrorWithSourceLoc(clangSyntaxCmd, - env.run(clangSyntaxCmd, env.stdout(), env.stderr())) + + stdinBuffer := &bytes.Buffer{} + exitCode, err = wrapSubprocessErrorWithSourceLoc(clangSyntaxCmd, + env.run(clangSyntaxCmd, io.TeeReader(env.stdin(), stdinBuffer), env.stdout(), env.stderr())) + if err != nil || exitCode != 0 { + return exitCode, err + } + return wrapSubprocessErrorWithSourceLoc(gccCmd, + env.run(gccCmd, bytes.NewReader(stdinBuffer.Bytes()), env.stdout(), env.stderr())) } diff --git a/compiler_wrapper/clang_syntax_flag_test.go b/compiler_wrapper/clang_syntax_flag_test.go index 822561c2..8ee9c223 100644 --- a/compiler_wrapper/clang_syntax_flag_test.go +++ b/compiler_wrapper/clang_syntax_flag_test.go @@ -15,7 +15,7 @@ import ( func TestCheckClangSyntaxByNestedCall(t *testing.T) { withTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 1 { if err := verifyPath(cmd, "usr/bin/clang"); err != nil { return err @@ -42,7 +42,7 @@ func TestCheckClangSyntaxByNestedCall(t *testing.T) { func TestForwardStdOutAndStderrFromClangSyntaxCheck(t *testing.T) { withTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 1 { fmt.Fprint(stdout, "somemessage") fmt.Fprint(stderr, "someerror") @@ -60,9 +60,27 @@ func TestForwardStdOutAndStderrFromClangSyntaxCheck(t *testing.T) { }) } +func TestForwardStdinToClangSyntaxCheck(t *testing.T) { + withTestContext(t, func(ctx *testContext) { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + // Note: This is called for the clang syntax call as well as for + // the gcc call, and we assert that stdin is cloned and forwarded + // to both. + stdinStr := ctx.readAllString(stdin) + if stdinStr != "someinput" { + return fmt.Errorf("unexpected stdin. Got: %s", stdinStr) + } + return nil + } + io.WriteString(&ctx.stdinBuffer, "someinput") + ctx.must(callCompiler(ctx, ctx.cfg, + ctx.newCommand(gccX86_64, "-clang-syntax", "-", mainCc))) + }) +} + func TestForwardExitCodeFromClangSyntaxCheck(t *testing.T) { withTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 1 { return newExitCodeError(23) } @@ -78,7 +96,7 @@ func TestForwardExitCodeFromClangSyntaxCheck(t *testing.T) { func TestReportGeneralErrorsFromClangSyntaxCheck(t *testing.T) { withTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 1 { return errors.New("someerror") } @@ -97,7 +115,7 @@ func TestReportGeneralErrorsFromClangSyntaxCheck(t *testing.T) { func TestIgnoreClangSyntaxCheckWhenCallingClang(t *testing.T) { withTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount > 1 { return fmt.Errorf("Unexpected call %#v", cmd) } @@ -117,7 +135,7 @@ func TestUseGomaForClangSyntaxCheck(t *testing.T) { // Create a file so the gomacc path is valid. ctx.writeFile(gomaPath, "") ctx.env = []string{"GOMACC_PATH=" + gomaPath} - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 1 { if err := verifyPath(cmd, gomaPath); err != nil { return err @@ -142,7 +160,7 @@ func TestUseGomaForClangSyntaxCheck(t *testing.T) { func TestPartiallyOmitCCacheForClangSyntaxCheck(t *testing.T) { withTestContext(t, func(ctx *testContext) { ctx.cfg.useCCache = true - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 1 { if err := verifyPath(cmd, "usr/bin/clang"); err != nil { return err diff --git a/compiler_wrapper/clang_tidy_flag.go b/compiler_wrapper/clang_tidy_flag.go index 5bd67064..616ff47c 100644 --- a/compiler_wrapper/clang_tidy_flag.go +++ b/compiler_wrapper/clang_tidy_flag.go @@ -77,8 +77,10 @@ func runClangTidy(env env, clangCmd *command, cSrcFile string) error { EnvUpdates: clangCmd.EnvUpdates, } + // Note: We pass nil as stdin as we checked before that the compiler + // was invoked with a source file argument. exitCode, err := wrapSubprocessErrorWithSourceLoc(clangTidyCmd, - env.run(clangTidyCmd, env.stdout(), env.stderr())) + env.run(clangTidyCmd, nil, env.stdout(), env.stderr())) if err == nil && exitCode != 0 { // Note: We continue on purpose when clang-tidy fails // to maintain compatibility with the previous wrapper. diff --git a/compiler_wrapper/clang_tidy_flag_test.go b/compiler_wrapper/clang_tidy_flag_test.go index b2578c51..baf5219e 100644 --- a/compiler_wrapper/clang_tidy_flag_test.go +++ b/compiler_wrapper/clang_tidy_flag_test.go @@ -24,7 +24,7 @@ func TestClangTidyBasename(t *testing.T) { } var clangTidyCmd *command - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 2 { clangTidyCmd = cmd } @@ -48,7 +48,7 @@ func TestClangTidyBasename(t *testing.T) { func TestClangTidyClangResourceDir(t *testing.T) { withClangTidyTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: if err := verifyPath(cmd, "usr/bin/clang"); err != nil { @@ -87,7 +87,7 @@ func TestClangTidyClangResourceDir(t *testing.T) { func TestClangTidyArgOrder(t *testing.T) { withClangTidyTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 2 { if err := verifyArgOrder(cmd, "-checks=.*", mainCc, "--", "-resource-dir=.*", mainCc, "--some_arg"); err != nil { return err @@ -105,7 +105,7 @@ func TestClangTidyArgOrder(t *testing.T) { func TestForwardStdOutAndStderrFromClangTidyCall(t *testing.T) { withClangTidyTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 2 { fmt.Fprint(stdout, "somemessage") fmt.Fprint(stderr, "someerror") @@ -125,7 +125,7 @@ func TestForwardStdOutAndStderrFromClangTidyCall(t *testing.T) { func TestIgnoreNonZeroExitCodeFromClangTidy(t *testing.T) { withClangTidyTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 2 { return newExitCodeError(23) } @@ -142,7 +142,7 @@ func TestIgnoreNonZeroExitCodeFromClangTidy(t *testing.T) { func TestReportGeneralErrorsFromClangTidy(t *testing.T) { withClangTidyTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if ctx.cmdCount == 2 { return errors.New("someerror") } @@ -212,7 +212,7 @@ func TestOmitCCacheWithClangTidy(t *testing.T) { withClangTidyTestContext(t, func(ctx *testContext) { ctx.cfg.useCCache = true - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: if err := verifyPath(cmd, "usr/bin/clang"); err != nil { @@ -246,7 +246,7 @@ func TestPartiallyOmitGomaWithClangTidy(t *testing.T) { ctx.writeFile(gomaPath, "") ctx.env = append(ctx.env, "GOMACC_PATH="+gomaPath) - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: if err := verifyPath(cmd, "usr/bin/clang"); err != nil { diff --git a/compiler_wrapper/compiler_wrapper.go b/compiler_wrapper/compiler_wrapper.go index 4aa91c6c..429484c9 100644 --- a/compiler_wrapper/compiler_wrapper.go +++ b/compiler_wrapper/compiler_wrapper.go @@ -5,6 +5,7 @@ package main import ( + "bytes" "fmt" "io" "path/filepath" @@ -40,14 +41,16 @@ func callCompiler(env env, cfg *config, inputCmd *command) int { } func callCompilerWithRunAndCompareToOldWrapper(env env, cfg *config, inputCmd *command) (exitCode int, err error) { + stdinBuffer := &bytes.Buffer{} recordingEnv := &commandRecordingEnv{ - env: env, + env: env, + stdinReader: io.TeeReader(env.stdin(), stdinBuffer), } // Note: this won't do a real exec as recordingEnv redirects exec to run. if exitCode, err = callCompilerInternal(recordingEnv, cfg, inputCmd); err != nil { return 0, err } - if err = compareToOldWrapper(env, cfg, inputCmd, recordingEnv.cmdResults, exitCode); err != nil { + if err = compareToOldWrapper(env, cfg, inputCmd, stdinBuffer.Bytes(), recordingEnv.cmdResults, exitCode); err != nil { return exitCode, err } return exitCode, nil @@ -89,10 +92,8 @@ func callCompilerInternal(env env, cfg *config, inputCmd *command) (exitCode int if err != nil { return 0, err } - exitCode, err = checkClangSyntax(env, clangCmd) - if err != nil || exitCode != 0 { - return exitCode, err - } + gccCmd := calcGccCommand(mainBuilder) + return checkClangSyntax(env, clangCmd, gccCmd) } compilerCmd = calcGccCommand(mainBuilder) } diff --git a/compiler_wrapper/compiler_wrapper_test.go b/compiler_wrapper/compiler_wrapper_test.go index 57bef951..71cd36df 100644 --- a/compiler_wrapper/compiler_wrapper_test.go +++ b/compiler_wrapper/compiler_wrapper_test.go @@ -58,7 +58,7 @@ func TestLogGeneralExecError(t *testing.T) { ctx.cfg.oldWrapperPath = testOldWrapperPath // Note: No need to write the old wrapper as we don't execute // it due to the general error from the new error. - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { return errors.New("someerror") } stderr := ctx.mustFail(callCompiler(ctx, ctx.cfg, ctx.newCommand(gccX86_64, mainCc))) @@ -75,6 +75,20 @@ func TestLogGeneralExecError(t *testing.T) { }) } +func TestForwardStdin(t *testing.T) { + withTestContext(t, func(ctx *testContext) { + io.WriteString(&ctx.stdinBuffer, "someinput") + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + stdinStr := ctx.readAllString(stdin) + if stdinStr != "someinput" { + return fmt.Errorf("unexpected stdin. Got: %s", stdinStr) + } + return nil + } + ctx.must(callCompiler(ctx, ctx.cfg, ctx.newCommand(gccX86_64, "-", mainCc))) + }) +} + func TestLogMissingCCacheExecError(t *testing.T) { withTestContext(t, func(ctx *testContext) { ctx.cfg.useCCache = true @@ -87,7 +101,7 @@ func TestLogMissingCCacheExecError(t *testing.T) { ctx.cfg.oldWrapperPath = testOldWrapperPath // Note: No need to write the old wrapper as we don't execute // it due to the general error from the new error. - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { return syscall.ENOENT } ctx.stderrBuffer.Reset() @@ -104,7 +118,7 @@ func TestLogExitCodeErrorWhenComparingToOldWrapper(t *testing.T) { ctx.cfg.mockOldWrapperCmds = false ctx.cfg.oldWrapperPath = filepath.Join(ctx.tempDir, "fakewrapper") - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { writePythonMockWrapper(ctx, &mockWrapperConfig{ Cmds: []*mockWrapperCmd{ { diff --git a/compiler_wrapper/disable_werror_flag.go b/compiler_wrapper/disable_werror_flag.go index 063e7571..809b8b6f 100644 --- a/compiler_wrapper/disable_werror_flag.go +++ b/compiler_wrapper/disable_werror_flag.go @@ -7,6 +7,7 @@ package main import ( "bytes" "encoding/json" + "io" "io/ioutil" "os" "strings" @@ -24,8 +25,9 @@ func doubleBuildWithWNoError(env env, cfg *config, originalCmd *command) (exitCo if originalCmd.Path == "/usr/bin/ccache" { originalCmd.Path = "ccache" } + originalStdinBuffer := &bytes.Buffer{} originalExitCode, err := wrapSubprocessErrorWithSourceLoc(originalCmd, - env.run(originalCmd, originalStdoutBuffer, originalStderrBuffer)) + env.run(originalCmd, io.TeeReader(env.stdin(), originalStdinBuffer), originalStdoutBuffer, originalStderrBuffer)) if err != nil { return 0, err } @@ -45,7 +47,7 @@ func doubleBuildWithWNoError(env env, cfg *config, originalCmd *command) (exitCo EnvUpdates: originalCmd.EnvUpdates, } retryExitCode, err := wrapSubprocessErrorWithSourceLoc(retryCommand, - env.run(retryCommand, retryStdoutBuffer, retryStderrBuffer)) + env.run(retryCommand, bytes.NewReader(originalStdinBuffer.Bytes()), retryStdoutBuffer, retryStderrBuffer)) if err != nil { return 0, err } diff --git a/compiler_wrapper/disable_werror_flag_test.go b/compiler_wrapper/disable_werror_flag_test.go index 066157d6..40d0f8c7 100644 --- a/compiler_wrapper/disable_werror_flag_test.go +++ b/compiler_wrapper/disable_werror_flag_test.go @@ -27,7 +27,7 @@ func TestOmitDoubleBuildForSuccessfulCall(t *testing.T) { func TestOmitDoubleBuildForGeneralError(t *testing.T) { withForceDisableWErrorTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { return errors.New("someerror") } stderr := ctx.mustFail(callCompiler(ctx, ctx.cfg, ctx.newCommand(clangX86_64, mainCc))) @@ -45,7 +45,7 @@ func TestOmitDoubleBuildForGeneralError(t *testing.T) { func TestDoubleBuildWithWNoErrorFlag(t *testing.T) { withForceDisableWErrorTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: if err := verifyArgCount(cmd, 0, "-Wno-error"); err != nil { @@ -73,7 +73,7 @@ func TestDoubleBuildWithWNoErrorFlag(t *testing.T) { func TestDoubleBuildWithWNoErrorAndCCache(t *testing.T) { withForceDisableWErrorTestContext(t, func(ctx *testContext) { ctx.cfg.useCCache = true - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: // TODO: This is a bug in the old wrapper that it drops the ccache path @@ -102,7 +102,7 @@ func TestDoubleBuildWithWNoErrorAndCCache(t *testing.T) { func TestForwardStdoutAndStderrWhenDoubleBuildSucceeds(t *testing.T) { withForceDisableWErrorTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: fmt.Fprint(stdout, "originalmessage") @@ -129,7 +129,7 @@ func TestForwardStdoutAndStderrWhenDoubleBuildSucceeds(t *testing.T) { func TestForwardStdoutAndStderrWhenDoubleBuildFails(t *testing.T) { withForceDisableWErrorTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: fmt.Fprint(stdout, "originalmessage") @@ -157,9 +157,36 @@ func TestForwardStdoutAndStderrWhenDoubleBuildFails(t *testing.T) { }) } +func TestForwardStdinFromDoubleBuild(t *testing.T) { + withForceDisableWErrorTestContext(t, func(ctx *testContext) { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + // Note: This is called for the clang syntax call as well as for + // the gcc call, and we assert that stdin is cloned and forwarded + // to both. + stdinStr := ctx.readAllString(stdin) + if stdinStr != "someinput" { + return fmt.Errorf("unexpected stdin. Got: %s", stdinStr) + } + + switch ctx.cmdCount { + case 1: + fmt.Fprint(stderr, "-Werror originalerror") + return newExitCodeError(1) + case 2: + return nil + default: + t.Fatalf("unexpected command: %#v", cmd) + return nil + } + } + io.WriteString(&ctx.stdinBuffer, "someinput") + ctx.must(callCompiler(ctx, ctx.cfg, ctx.newCommand(clangX86_64, "-", mainCc))) + }) +} + func TestForwardGeneralErrorWhenDoubleBuildFails(t *testing.T) { withForceDisableWErrorTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: fmt.Fprint(stderr, "-Werror originalerror") @@ -195,7 +222,7 @@ func TestOmitLogWarningsIfNoDoubleBuild(t *testing.T) { func TestLogWarningsWhenDoubleBuildSucceeds(t *testing.T) { withForceDisableWErrorTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: fmt.Fprint(stdout, "originalmessage") @@ -233,7 +260,7 @@ func TestLogWarningsWhenDoubleBuildSucceeds(t *testing.T) { func TestLogWarningsWhenDoubleBuildFails(t *testing.T) { withForceDisableWErrorTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { switch ctx.cmdCount { case 1: fmt.Fprint(stdout, "originalmessage") diff --git a/compiler_wrapper/env.go b/compiler_wrapper/env.go index 40bc2e41..bbf3ec45 100644 --- a/compiler_wrapper/env.go +++ b/compiler_wrapper/env.go @@ -17,9 +17,10 @@ type env interface { getenv(key string) string environ() []string getwd() string + stdin() io.Reader stdout() io.Writer stderr() io.Writer - run(cmd *command, stdout io.Writer, stderr io.Writer) error + run(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error exec(cmd *command) error } @@ -49,6 +50,10 @@ func (env *processEnv) getwd() string { return env.wd } +func (env *processEnv) stdin() io.Reader { + return os.Stdin +} + func (env *processEnv) stdout() io.Writer { return os.Stdout } @@ -62,16 +67,36 @@ func (env *processEnv) exec(cmd *command) error { return syscall.Exec(execCmd.Path, execCmd.Args, execCmd.Env) } -func (env *processEnv) run(cmd *command, stdout io.Writer, stderr io.Writer) error { +func (env *processEnv) run(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { execCmd := newExecCmd(env, cmd) + execCmd.Stdin = stdin execCmd.Stdout = stdout execCmd.Stderr = stderr + _, stdinIsFile := stdin.(*os.File) + _, stdinIsBytesReader := stdin.(*bytes.Reader) + if stdin != nil && !stdinIsFile && !stdinIsBytesReader { + // We can't use execCmd.Run() here as that blocks if stdin blocks, + // even if the underlying process has already terminated. We care + // especially about the case when stdin is an io.TeeReader for os.Stdin. + // See https://github.com/golang/go/issues/7990 for more details. + if err := execCmd.Start(); err != nil { + return err + } + if _, err := execCmd.Process.Wait(); err != nil { + return err + } + // Closing Stdin here as we didn't wait for the read to finish via + // execCmd.Wait to prevent race conditions. + os.Stdin.Close() + return nil + } return execCmd.Run() } type commandRecordingEnv struct { env - cmdResults []*commandResult + stdinReader io.Reader + cmdResults []*commandResult } type commandResult struct { Cmd *command `json:"cmd"` @@ -82,16 +107,20 @@ type commandResult struct { var _ env = (*commandRecordingEnv)(nil) +func (env *commandRecordingEnv) stdin() io.Reader { + return env.stdinReader +} + func (env *commandRecordingEnv) exec(cmd *command) error { // Note: We treat exec the same as run so that we can do work // after the call. - return env.run(cmd, env.stdout(), env.stderr()) + return env.run(cmd, env.stdin(), env.stdout(), env.stderr()) } -func (env *commandRecordingEnv) run(cmd *command, stdout io.Writer, stderr io.Writer) error { +func (env *commandRecordingEnv) run(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { stdoutBuffer := &bytes.Buffer{} stderrBuffer := &bytes.Buffer{} - err := env.env.run(cmd, io.MultiWriter(stdout, stdoutBuffer), io.MultiWriter(stderr, stderrBuffer)) + err := env.env.run(cmd, stdin, io.MultiWriter(stdout, stdoutBuffer), io.MultiWriter(stderr, stderrBuffer)) if exitCode, ok := getExitCode(err); ok { env.cmdResults = append(env.cmdResults, &commandResult{ Cmd: cmd, @@ -114,9 +143,9 @@ func (env *printingEnv) exec(cmd *command) error { return env.env.exec(cmd) } -func (env *printingEnv) run(cmd *command, stdout io.Writer, stderr io.Writer) error { +func (env *printingEnv) run(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { printCmd(env, cmd) - return env.env.run(cmd, stdout, stderr) + return env.env.run(cmd, stdin, stdout, stderr) } func printCmd(env env, cmd *command) { diff --git a/compiler_wrapper/goldenutil_test.go b/compiler_wrapper/goldenutil_test.go index 99d45bb8..9c164442 100644 --- a/compiler_wrapper/goldenutil_test.go +++ b/compiler_wrapper/goldenutil_test.go @@ -126,7 +126,7 @@ func fillGoldenResults(ctx *testContext, files []goldenFile) []goldenFile { newRecords := []goldenRecord{} for _, record := range file.Records { newCmds := []commandResult{} - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { if len(newCmds) >= len(record.Cmds) { ctx.t.Errorf("Not enough commands specified for wrapperCmd %#v and env %#v. Expected: %#v", record.WrapperCmd.Cmd, record.Env, record.Cmds) diff --git a/compiler_wrapper/oldwrapper.go b/compiler_wrapper/oldwrapper.go index 1be4db5a..8ae6f05c 100644 --- a/compiler_wrapper/oldwrapper.go +++ b/compiler_wrapper/oldwrapper.go @@ -20,7 +20,7 @@ import ( const compareToOldWrapperFilePattern = "old_wrapper_compare" -func compareToOldWrapper(env env, cfg *config, inputCmd *command, newCmdResults []*commandResult, newExitCode int) error { +func compareToOldWrapper(env env, cfg *config, inputCmd *command, stdinBuffer []byte, newCmdResults []*commandResult, newExitCode int) error { pythonStringEscaper := strings.NewReplacer("\n", "\\n", "'", "\\'") oldWrapperCfg, err := newOldWrapperConfig(env, cfg, inputCmd) @@ -41,9 +41,9 @@ func compareToOldWrapper(env env, cfg *config, inputCmd *command, newCmdResults stderrBuffer := bytes.Buffer{} oldExitCode := 0 if strings.HasPrefix(oldWrapperCfg.OldWrapperContent, "#!/bin/sh") { - oldExitCode, err = callOldShellWrapper(env, oldWrapperCfg, inputCmd, compareToOldWrapperFilePattern, &bytes.Buffer{}, &stderrBuffer) + oldExitCode, err = callOldShellWrapper(env, oldWrapperCfg, inputCmd, stdinBuffer, compareToOldWrapperFilePattern, &bytes.Buffer{}, &stderrBuffer) } else { - oldExitCode, err = callOldPythonWrapper(env, oldWrapperCfg, inputCmd, compareToOldWrapperFilePattern, &bytes.Buffer{}, &stderrBuffer) + oldExitCode, err = callOldPythonWrapper(env, oldWrapperCfg, inputCmd, stdinBuffer, compareToOldWrapperFilePattern, &bytes.Buffer{}, &stderrBuffer) } if err != nil { return err @@ -193,7 +193,7 @@ func newOldWrapperConfig(env env, cfg *config, inputCmd *command) (*oldWrapperCo }, nil } -func callOldShellWrapper(env env, cfg *oldWrapperConfig, inputCmd *command, filepattern string, stdout io.Writer, stderr io.Writer) (exitCode int, err error) { +func callOldShellWrapper(env env, cfg *oldWrapperConfig, inputCmd *command, stdinBuffer []byte, filepattern string, stdout io.Writer, stderr io.Writer) (exitCode int, err error) { oldWrapperContent := cfg.OldWrapperContent oldWrapperContent = regexp.MustCompile(`(?m)^exec\b`).ReplaceAllString(oldWrapperContent, "exec_mock") oldWrapperContent = regexp.MustCompile(`\$EXEC`).ReplaceAllString(oldWrapperContent, "exec_mock") @@ -243,10 +243,10 @@ function exec_mock { Args: append([]string{mockFile.Name()}, inputCmd.Args...), EnvUpdates: inputCmd.EnvUpdates, } - return wrapSubprocessErrorWithSourceLoc(oldWrapperCmd, env.run(oldWrapperCmd, stdout, stderr)) + return wrapSubprocessErrorWithSourceLoc(oldWrapperCmd, env.run(oldWrapperCmd, bytes.NewReader(stdinBuffer), stdout, stderr)) } -func callOldPythonWrapper(env env, cfg *oldWrapperConfig, inputCmd *command, filepattern string, stdout io.Writer, stderr io.Writer) (exitCode int, err error) { +func callOldPythonWrapper(env env, cfg *oldWrapperConfig, inputCmd *command, stdinBuffer []byte, filepattern string, stdout io.Writer, stderr io.Writer) (exitCode int, err error) { oldWrapperContent := cfg.OldWrapperContent // TODO: Use strings.ReplaceAll once cros sdk uses golang >= 1.12 oldWrapperContent = strings.Replace(oldWrapperContent, "from __future__ import print_function", "", -1) @@ -380,5 +380,5 @@ runMain() Args: append([]string{"-S", mockFile.Name()}, inputCmd.Args...), EnvUpdates: inputCmd.EnvUpdates, } - return wrapSubprocessErrorWithSourceLoc(oldWrapperCmd, env.run(oldWrapperCmd, stdout, stderr)) + return wrapSubprocessErrorWithSourceLoc(oldWrapperCmd, env.run(oldWrapperCmd, bytes.NewReader(stdinBuffer), stdout, stderr)) } diff --git a/compiler_wrapper/oldwrapper_test.go b/compiler_wrapper/oldwrapper_test.go index a07615a9..e1752ce0 100644 --- a/compiler_wrapper/oldwrapper_test.go +++ b/compiler_wrapper/oldwrapper_test.go @@ -6,6 +6,7 @@ package main import ( "bytes" + "fmt" "io" "path/filepath" "strings" @@ -31,7 +32,7 @@ func TestCompareToOldPythonWrapperCompilerCommand(t *testing.T) { newWrapperExitCode = 0 } - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { writePythonMockWrapper(ctx, &mockWrapperConfig{ Cmds: []*mockWrapperCmd{ { @@ -92,7 +93,7 @@ func TestCompareToOldPythonWrapperNestedCommand(t *testing.T) { extraArgs := []string{} wrapperCfg := &mockWrapperConfig{} - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { isNestedCmd := len(wrapperCfg.Cmds) == 0 var wrapperCmd *mockWrapperCmd if isNestedCmd { @@ -165,7 +166,7 @@ func TestCompareToOldShellWrapperCompilerCommand(t *testing.T) { newWrapperExitCode = 0 } - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { writeShellMockWrapper(ctx, &mockWrapperConfig{ Cmds: []*mockWrapperCmd{ { @@ -222,7 +223,7 @@ func TestCompareToOldWrapperEscapeStdoutAndStderr(t *testing.T) { ctx.cfg.mockOldWrapperCmds = false ctx.cfg.oldWrapperPath = filepath.Join(ctx.tempDir, "fakewrapper") - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { io.WriteString(stdout, "a\n'b'") io.WriteString(stderr, "c\n'd'") writePythonMockWrapper(ctx, &mockWrapperConfig{ @@ -282,7 +283,7 @@ func TestCompareToOldPythonWrapperArgumentsWithSpaces(t *testing.T) { ctx.cfg.mockOldWrapperCmds = false ctx.cfg.oldWrapperPath = filepath.Join(ctx.tempDir, "fakewrapper") - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { writePythonMockWrapper(ctx, &mockWrapperConfig{ Cmds: []*mockWrapperCmd{ { @@ -304,7 +305,7 @@ func TestCompareToOldShellWrapperArgumentsWithSpaces(t *testing.T) { ctx.cfg.mockOldWrapperCmds = false ctx.cfg.oldWrapperPath = filepath.Join(ctx.tempDir, "fakewrapper") - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { writeShellMockWrapper(ctx, &mockWrapperConfig{ Cmds: []*mockWrapperCmd{ { @@ -321,6 +322,33 @@ func TestCompareToOldShellWrapperArgumentsWithSpaces(t *testing.T) { }) } +func TestForwardStdinWhenUsingOldWrapper(t *testing.T) { + withTestContext(t, func(ctx *testContext) { + io.WriteString(&ctx.stdinBuffer, "someinput") + ctx.cfg.mockOldWrapperCmds = false + ctx.cfg.oldWrapperPath = filepath.Join(ctx.tempDir, "fakewrapper") + + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + writeShellMockWrapper(ctx, &mockWrapperConfig{ + Cmds: []*mockWrapperCmd{ + { + Path: cmd.Path, + Args: cmd.Args, + }, + }, + }) + stdinStr := ctx.readAllString(stdin) + if stdinStr != "someinput" { + return fmt.Errorf("unexpected stdin. Got: %s", stdinStr) + } + return nil + } + + ctx.must(callCompiler(ctx, ctx.cfg, + ctx.newCommand(clangX86_64, "-", mainCc))) + }) +} + func writePythonMockWrapper(ctx *testContext, cfg *mockWrapperConfig) { const mockTemplate = ` from __future__ import print_function diff --git a/compiler_wrapper/rusage_flag.go b/compiler_wrapper/rusage_flag.go index b1c2c45e..c3337364 100644 --- a/compiler_wrapper/rusage_flag.go +++ b/compiler_wrapper/rusage_flag.go @@ -29,7 +29,7 @@ func logRusage(env env, logFileName string, compilerCmd *command) (exitCode int, } startTime := time.Now() exitCode, err = wrapSubprocessErrorWithSourceLoc(compilerCmdWithoutRusage, - env.run(compilerCmdWithoutRusage, env.stdout(), env.stderr())) + env.run(compilerCmdWithoutRusage, env.stdin(), env.stdout(), env.stderr())) if err != nil { return 0, err } diff --git a/compiler_wrapper/rusage_flag_test.go b/compiler_wrapper/rusage_flag_test.go index 916a5332..7acba0c8 100644 --- a/compiler_wrapper/rusage_flag_test.go +++ b/compiler_wrapper/rusage_flag_test.go @@ -19,7 +19,7 @@ import ( func TestForwardStdOutAndStdErrAndExitCodeFromLogRusage(t *testing.T) { withLogRusageTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { fmt.Fprint(stdout, "somemessage") fmt.Fprint(stderr, "someerror") return newExitCodeError(23) @@ -37,9 +37,26 @@ func TestForwardStdOutAndStdErrAndExitCodeFromLogRusage(t *testing.T) { }) } +func TestForwardStdinFromLogRusage(t *testing.T) { + withLogRusageTestContext(t, func(ctx *testContext) { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + // Note: This is called for the clang syntax call as well as for + // the gcc call, and we assert that stdin is cloned and forwarded + // to both. + stdinStr := ctx.readAllString(stdin) + if stdinStr != "someinput" { + return fmt.Errorf("unexpected stdin. Got: %s", stdinStr) + } + return nil + } + io.WriteString(&ctx.stdinBuffer, "someinput") + ctx.must(callCompiler(ctx, ctx.cfg, ctx.newCommand(clangX86_64, "-", mainCc))) + }) +} + func TestReportGeneralErrorsFromLogRusage(t *testing.T) { withLogRusageTestContext(t, func(ctx *testContext) { - ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { + ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { return errors.New("someerror") } stderr := ctx.mustFail(callCompiler(ctx, ctx.cfg, diff --git a/compiler_wrapper/testutil_test.go b/compiler_wrapper/testutil_test.go index 778c4c26..3e568c77 100644 --- a/compiler_wrapper/testutil_test.go +++ b/compiler_wrapper/testutil_test.go @@ -38,7 +38,8 @@ type testContext struct { inputCmd *command lastCmd *command cmdCount int - cmdMock func(cmd *command, stdout io.Writer, stderr io.Writer) error + cmdMock func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error + stdinBuffer bytes.Buffer stdoutBuffer bytes.Buffer stderrBuffer bytes.Buffer } @@ -83,6 +84,10 @@ func (ctx *testContext) getwd() string { return ctx.wd } +func (ctx *testContext) stdin() io.Reader { + return &ctx.stdinBuffer +} + func (ctx *testContext) stdout() io.Writer { return &ctx.stdoutBuffer } @@ -99,7 +104,7 @@ func (ctx *testContext) stderrString() string { return ctx.stderrBuffer.String() } -func (ctx *testContext) run(cmd *command, stdout io.Writer, stderr io.Writer) error { +func (ctx *testContext) run(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { // Keep calling the old wrapper when we are comparing the output of the // old wrapper to the new wrapper. if isCompareToOldWrapperCmd(cmd) { @@ -111,7 +116,7 @@ func (ctx *testContext) run(cmd *command, stdout io.Writer, stderr io.Writer) er ctx.cmdCount++ ctx.lastCmd = cmd if ctx.cmdMock != nil { - return ctx.cmdMock(cmd, stdout, stderr) + return ctx.cmdMock(cmd, stdin, stdout, stderr) } return nil } @@ -120,7 +125,7 @@ func (ctx *testContext) exec(cmd *command) error { ctx.cmdCount++ ctx.lastCmd = cmd if ctx.cmdMock != nil { - return ctx.cmdMock(cmd, ctx.stdout(), ctx.stderr()) + return ctx.cmdMock(cmd, ctx.stdin(), ctx.stdout(), ctx.stderr()) } return nil } @@ -188,6 +193,17 @@ func (ctx *testContext) symlink(oldname string, newname string) { } } +func (ctx *testContext) readAllString(r io.Reader) string { + if r == nil { + return "" + } + bytes, err := ioutil.ReadAll(r) + if err != nil { + ctx.t.Fatal(err) + } + return string(bytes) +} + func verifyPath(cmd *command, expectedRegex string) error { compiledRegex := regexp.MustCompile(matchFullString(expectedRegex)) if !compiledRegex.MatchString(cmd.Path) { |