diff --git a/internal/cli/compile/compile.go b/internal/cli/compile/compile.go index e128155eeb8..c790d493222 100644 --- a/internal/cli/compile/compile.go +++ b/internal/cli/compile/compile.go @@ -144,9 +144,6 @@ func NewCommand() *cobra.Command { func runCompileCommand(cmd *cobra.Command, args []string) { logrus.Info("Executing `arduino-cli compile`") - if dumpProfile && feedback.GetFormat() != feedback.Text { - feedback.Fatal(tr("You cannot use the %[1]s flag together with %[2]s.", "--dump-profile", "--format json"), feedback.ErrBadArgument) - } if profileArg.Get() != "" { if len(libraries) > 0 { feedback.Fatal(tr("You cannot use the %s flag while compiling with a profile.", "--libraries"), feedback.ErrBadArgument) @@ -255,7 +252,10 @@ func runCompileCommand(cmd *cobra.Command, args []string) { } } - if dumpProfile { + profileOut := "" + if dumpProfile && compileError == nil { + // Output profile + libs := "" hasVendoredLibs := false for _, lib := range compileRes.GetUsedLibraries() { @@ -279,47 +279,42 @@ func runCompileCommand(cmd *cobra.Command, args []string) { if split := strings.Split(compileRequest.GetFqbn(), ":"); len(split) > 2 { newProfileName = split[2] } - profile := fmt.Sprintln() - profile += fmt.Sprintln("profiles:") - profile += fmt.Sprintln(" " + newProfileName + ":") - profile += fmt.Sprintln(" fqbn: " + compileRequest.GetFqbn()) - profile += fmt.Sprintln(" platforms:") + profileOut = fmt.Sprintln("profiles:") + profileOut += fmt.Sprintln(" " + newProfileName + ":") + profileOut += fmt.Sprintln(" fqbn: " + compileRequest.GetFqbn()) + profileOut += fmt.Sprintln(" platforms:") boardPlatform := compileRes.GetBoardPlatform() - profile += fmt.Sprintln(" - platform: " + boardPlatform.GetId() + " (" + boardPlatform.GetVersion() + ")") + profileOut += fmt.Sprintln(" - platform: " + boardPlatform.GetId() + " (" + boardPlatform.GetVersion() + ")") if url := boardPlatform.GetPackageUrl(); url != "" { - profile += fmt.Sprintln(" platform_index_url: " + url) + profileOut += fmt.Sprintln(" platform_index_url: " + url) } if buildPlatform := compileRes.GetBuildPlatform(); buildPlatform != nil && buildPlatform.Id != boardPlatform.Id && buildPlatform.Version != boardPlatform.Version { - profile += fmt.Sprintln(" - platform: " + buildPlatform.GetId() + " (" + buildPlatform.GetVersion() + ")") + profileOut += fmt.Sprintln(" - platform: " + buildPlatform.GetId() + " (" + buildPlatform.GetVersion() + ")") if url := buildPlatform.GetPackageUrl(); url != "" { - profile += fmt.Sprintln(" platform_index_url: " + url) + profileOut += fmt.Sprintln(" platform_index_url: " + url) } } if len(libs) > 0 { - profile += fmt.Sprintln(" libraries:") - profile += fmt.Sprint(libs) + profileOut += fmt.Sprintln(" libraries:") + profileOut += fmt.Sprint(libs) } - - // Output profile as a result - if _, err := stdOut.Write([]byte(profile)); err != nil { - feedback.FatalError(err, feedback.ErrGeneric) - } - feedback.PrintResult(stdIORes()) - return + profileOut += fmt.Sprintln() } stdIO := stdIORes() - feedback.PrintResult(&compileResult{ + res := &compileResult{ CompilerOut: stdIO.Stdout, CompilerErr: stdIO.Stderr, BuilderResult: compileRes, + ProfileOut: profileOut, Success: compileError == nil, - }) + } + if compileError != nil { - msg := tr("Error during build: %v", compileError) + res.Error = tr("Error during build: %v", compileError) // Check the error type to give the user better feedback on how // to resolve it @@ -339,17 +334,18 @@ func runCompileCommand(cmd *cobra.Command, args []string) { release() if profileArg.String() == "" { - msg += "\n" + res.Error += fmt.Sprintln() if platform != nil { suggestion := fmt.Sprintf("`%s core install %s`", version.VersionInfo.Application, platformErr.Platform) - msg += tr("Try running %s", suggestion) + res.Error += tr("Try running %s", suggestion) } else { - msg += tr("Platform %s is not found in any known index\nMaybe you need to add a 3rd party URL?", platformErr.Platform) + res.Error += tr("Platform %s is not found in any known index\nMaybe you need to add a 3rd party URL?", platformErr.Platform) } } } - feedback.Fatal(msg, feedback.ErrGeneric) + feedback.FatalResult(res, feedback.ErrGeneric) } + feedback.PrintResult(res) } type compileResult struct { @@ -357,6 +353,8 @@ type compileResult struct { CompilerErr string `json:"compiler_err"` BuilderResult *rpc.CompileResponse `json:"builder_result"` Success bool `json:"success"` + ProfileOut string `json:"profile_out,omitempty"` + Error string `json:"error,omitempty"` } func (r *compileResult) Data() interface{} { @@ -369,9 +367,12 @@ func (r *compileResult) String() string { pathColor := color.New(color.FgHiBlack) build := r.BuilderResult - res := "\n" - libraries := table.New() + res := "" + if r.CompilerOut != "" || r.CompilerErr != "" { + res += fmt.Sprintln() + } if len(build.GetUsedLibraries()) > 0 { + libraries := table.New() libraries.SetHeader( table.NewCell(tr("Used library"), titleColor), table.NewCell(tr("Version"), titleColor), @@ -382,8 +383,8 @@ func (r *compileResult) String() string { l.GetVersion(), table.NewCell(l.GetInstallDir(), pathColor)) } + res += fmt.Sprintln(libraries.Render()) } - res += libraries.Render() + "\n" if boardPlatform := build.GetBoardPlatform(); boardPlatform != nil { platforms := table.New() @@ -403,7 +404,14 @@ func (r *compileResult) String() string { buildPlatform.GetVersion(), table.NewCell(buildPlatform.GetInstallDir(), pathColor)) } - res += platforms.Render() + res += fmt.Sprintln(platforms.Render()) + } + if r.ProfileOut != "" { + res += fmt.Sprintln(r.ProfileOut) } - return res + return strings.TrimRight(res, fmt.Sprintln()) +} + +func (r *compileResult) ErrorString() string { + return r.Error } diff --git a/internal/cli/completion/completion.go b/internal/cli/completion/completion.go index d119d57f120..32b337eaf41 100644 --- a/internal/cli/completion/completion.go +++ b/internal/cli/completion/completion.go @@ -47,8 +47,11 @@ func NewCommand() *cobra.Command { } func runCompletionCommand(cmd *cobra.Command, args []string) { - stdOut, _, res := feedback.OutputStreams() logrus.Info("Executing `arduino-cli completion`") + stdOut, _, err := feedback.DirectStreams() + if err != nil { + feedback.Fatal(err.Error(), feedback.ErrGeneric) + } if completionNoDesc && (args[0] == "powershell") { feedback.Fatal(tr("Error: command description is not supported by %v", args[0]), feedback.ErrGeneric) } @@ -66,5 +69,4 @@ func runCompletionCommand(cmd *cobra.Command, args []string) { case "powershell": cmd.Root().GenPowerShellCompletion(stdOut) } - feedback.PrintResult(res()) } diff --git a/internal/cli/daemon/daemon.go b/internal/cli/daemon/daemon.go index 16c6d9af83a..9536510ff8e 100644 --- a/internal/cli/daemon/daemon.go +++ b/internal/cli/daemon/daemon.go @@ -89,12 +89,11 @@ func runDaemonCommand(cmd *cobra.Command, args []string) { defer f.Close() debugStdOut = f } else { - // Attach to os.Stdout only if we are in Text mode - if feedback.GetFormat() != feedback.Text { - feedback.Fatal(tr("Debug log is only available in text format"), feedback.ErrBadArgument) + if out, _, err := feedback.DirectStreams(); err != nil { + feedback.Fatal(tr("Can't write debug log: %s", err), feedback.ErrBadArgument) + } else { + debugStdOut = out } - out, _, _ := feedback.OutputStreams() - debugStdOut = out } gRPCOptions = append(gRPCOptions, grpc.UnaryInterceptor(unaryLoggerInterceptor), diff --git a/internal/cli/feedback/feedback.go b/internal/cli/feedback/feedback.go index 424bf63de09..7a2ff5087a5 100644 --- a/internal/cli/feedback/feedback.go +++ b/internal/cli/feedback/feedback.go @@ -102,6 +102,13 @@ type Result interface { Data() interface{} } +// ErrorResult is a result embedding also an error. In case of textual output +// the error will be printed on stderr. +type ErrorResult interface { + Result + ErrorString() string +} + var tr = i18n.Tr // SetOut can be used to change the out writer at runtime @@ -168,6 +175,12 @@ func FatalError(err error, exitCode ExitCode) { Fatal(err.Error(), exitCode) } +// FatalResult outputs the result and exits with status exitCode. +func FatalResult(res ErrorResult, exitCode ExitCode) { + PrintResult(res) + os.Exit(int(exitCode)) +} + // Fatal outputs the errorMsg and exits with status exitCode. func Fatal(errorMsg string, exitCode ExitCode) { if format == Text { @@ -179,7 +192,7 @@ func Fatal(errorMsg string, exitCode ExitCode) { Error string `json:"error"` Output *OutputStreamsResult `json:"output,omitempty"` } - res := FatalError{ + res := &FatalError{ Error: errorMsg, } if output := getOutputStreamResult(); !output.Empty() { @@ -188,11 +201,11 @@ func Fatal(errorMsg string, exitCode ExitCode) { var d []byte switch format { case JSON: - d, _ = json.MarshalIndent(res, "", " ") + d, _ = json.MarshalIndent(augment(res), "", " ") case MinifiedJSON: - d, _ = json.Marshal(res) + d, _ = json.Marshal(augment(res)) case YAML: - d, _ = yaml.Marshal(res) + d, _ = yaml.Marshal(augment(res)) default: panic("unknown output format") } @@ -223,6 +236,7 @@ func augment(data interface{}) interface{} { // structure. func PrintResult(res Result) { var data string + var dataErr string switch format { case JSON: d, err := json.MarshalIndent(augment(res.Data()), "", " ") @@ -244,10 +258,16 @@ func PrintResult(res Result) { data = string(d) case Text: data = res.String() + if resErr, ok := res.(ErrorResult); ok { + dataErr = resErr.ErrorString() + } default: panic("unknown output format") } if data != "" { fmt.Fprintln(stdOut, data) } + if dataErr != "" { + fmt.Fprintln(stdErr, dataErr) + } } diff --git a/internal/cli/feedback/stdio.go b/internal/cli/feedback/stdio.go index 3650c585908..787435462e1 100644 --- a/internal/cli/feedback/stdio.go +++ b/internal/cli/feedback/stdio.go @@ -16,17 +16,39 @@ package feedback import ( + "errors" "io" ) -// OutputStreams returns the underlying io.Writer to directly stream to +// DirectStreams returns the underlying io.Writer to directly stream to // stdout and stderr. -// If the selected output format is not Text, the returned writers will -// accumulate the output until command execution is completed. +// If the selected output format is not Text, the function will error. +// +// Using the streams returned by this function allows direct control of +// the output and the PrintResult function must not be used anymore +func DirectStreams() (io.Writer, io.Writer, error) { + if !formatSelected { + panic("output format not yet selected") + } + if format != Text { + return nil, nil, errors.New(tr("available only in text format")) + } + return stdOut, stdErr, nil +} + +// OutputStreams returns a pair of io.Writer to write the command output. +// The returned writers will accumulate the output until the command +// execution is completed, so they are not suitable for printing an unbounded +// stream like a debug logger or an event watcher (use DirectStreams for +// that purpose). +// +// If the output format is Text the output will be directly streamed to the +// underlying stdio streams in real time. +// // This function returns also a callback that must be called when the -// command execution is completed, it will return a *OutputStreamsResult -// object that can be used as a Result or to retrieve the output to embed -// it in another object. +// command execution is completed, it will return an *OutputStreamsResult +// object that can be used as a Result or to retrieve the accumulated output +// to embed it in another object. func OutputStreams() (io.Writer, io.Writer, func() *OutputStreamsResult) { if !formatSelected { panic("output format not yet selected") diff --git a/internal/integrationtest/compile_2/compile_test.go b/internal/integrationtest/compile_2/compile_test.go index c59c18d839e..c822481fbd7 100644 --- a/internal/integrationtest/compile_2/compile_test.go +++ b/internal/integrationtest/compile_2/compile_test.go @@ -421,16 +421,16 @@ func TestCompileNonInstalledPlatformWithWrongPackagerAndArch(t *testing.T) { require.NoError(t, err) // Compile with wrong packager - _, stderr, err := cli.Run("compile", "-b", "wrong:avr:uno", sketchPath.String()) + stdout, stderr, err := cli.Run("compile", "-b", "wrong:avr:uno", sketchPath.String()) require.Error(t, err) require.Contains(t, string(stderr), "Error during build: Platform 'wrong:avr' not found: platform not installed") - require.Contains(t, string(stderr), "Platform wrong:avr is not found in any known index") + require.Contains(t, string(stdout), "Platform wrong:avr is not found in any known index") // Compile with wrong arch - _, stderr, err = cli.Run("compile", "-b", "arduino:wrong:uno", sketchPath.String()) + stdout, stderr, err = cli.Run("compile", "-b", "arduino:wrong:uno", sketchPath.String()) require.Error(t, err) require.Contains(t, string(stderr), "Error during build: Platform 'arduino:wrong' not found: platform not installed") - require.Contains(t, string(stderr), "Platform arduino:wrong is not found in any known index") + require.Contains(t, string(stdout), "Platform arduino:wrong is not found in any known index") } func TestCompileWithKnownPlatformNotInstalled(t *testing.T) { @@ -446,9 +446,9 @@ func TestCompileWithKnownPlatformNotInstalled(t *testing.T) { require.NoError(t, err) // Try to compile using a platform found in the index but not installed - _, stderr, err := cli.Run("compile", "-b", "arduino:avr:uno", sketchPath.String()) + stdout, stderr, err := cli.Run("compile", "-b", "arduino:avr:uno", sketchPath.String()) require.Error(t, err) require.Contains(t, string(stderr), "Error during build: Platform 'arduino:avr' not found: platform not installed") // Verifies command to fix error is shown to user - require.Contains(t, string(stderr), "Try running `arduino-cli core install arduino:avr`") + require.Contains(t, string(stdout), "Try running `arduino-cli core install arduino:avr`") }