github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/tiltfile_result.go (about) 1 package cli 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "regexp" 8 "time" 9 10 "github.com/pkg/errors" 11 "github.com/spf13/cobra" 12 "k8s.io/cli-runtime/pkg/genericclioptions" 13 14 "github.com/tilt-dev/tilt/pkg/logger" 15 16 "github.com/tilt-dev/tilt/internal/analytics" 17 ctrltiltfile "github.com/tilt-dev/tilt/internal/controllers/apis/tiltfile" 18 "github.com/tilt-dev/tilt/internal/tiltfile" 19 "github.com/tilt-dev/tilt/pkg/model" 20 ) 21 22 var tupleRE = regexp.MustCompile(`,\)$`) 23 24 // arbitrary non-1 value chosen to allow callers to distinguish between 25 // Tilt errors and Tiltfile errors 26 const TiltfileErrExitCode = 5 27 28 type tiltfileResultCmd struct { 29 streams genericclioptions.IOStreams 30 exit func(code int) 31 32 fileName string 33 34 // for Builtin Timings mode 35 builtinTimings bool 36 durThreshold time.Duration 37 } 38 39 var _ tiltCmd = &tiltfileResultCmd{} 40 41 type cmdTiltfileResultDeps struct { 42 tfl tiltfile.TiltfileLoader 43 } 44 45 func newTiltfileResultDeps(tfl tiltfile.TiltfileLoader) cmdTiltfileResultDeps { 46 return cmdTiltfileResultDeps{ 47 tfl: tfl, 48 } 49 } 50 51 func newTiltfileResultCmd(streams genericclioptions.IOStreams) *tiltfileResultCmd { 52 return &tiltfileResultCmd{ 53 streams: streams, 54 exit: os.Exit, 55 } 56 } 57 58 func (c *tiltfileResultCmd) name() model.TiltSubcommand { return "tiltfile-result" } 59 60 func (c *tiltfileResultCmd) register() *cobra.Command { 61 cmd := &cobra.Command{ 62 Use: "tiltfile-result", 63 Short: "Exec the Tiltfile and print data about execution", 64 Long: `Exec the Tiltfile and print data about execution. 65 66 By default, prints Tiltfile execution results as JSON (note: the API is unstable and may change); can also print timings of Tiltfile Builtin calls. 67 68 Exit code 0: successful Tiltfile evaluation (data printed to stdout) 69 Exit code 1: some failure in setup, printing results, etc. (any logs printed to stderr) 70 Exit code 5: error when evaluating the Tiltfile, such as syntax error, illegal Tiltfile operation, etc. (any logs printed to stderr) 71 72 Run with -v | --verbose to print Tiltfile execution logs on stderr, regardless of whether there was an error.`, 73 } 74 75 addTiltfileFlag(cmd, &c.fileName) 76 addKubeContextFlag(cmd) 77 cmd.Flags().BoolVarP(&c.builtinTimings, "builtin-timings", "b", false, "If true, print timing data for Tiltfile builtin calls instead of Tiltfile result JSON") 78 cmd.Flags().DurationVar(&c.durThreshold, "dur-threshold", 0, "Only compatible with Builtin Timings mode. Should be a Go duration string. If passed, only print information about builtin calls lasting this duration and longer.") 79 80 return cmd 81 } 82 83 func (c *tiltfileResultCmd) run(ctx context.Context, args []string) error { 84 // HACK(maia): we're overloading the -v|--verbose flags here, which isn't ideal, 85 // but eh, it's fast. Might be cleaner to do --logs=true or something. 86 logLvl := logger.Get(ctx).Level() 87 showTiltfileLogs := logLvl.ShouldDisplay(logger.VerboseLvl) 88 89 if !showTiltfileLogs { 90 // defer Tiltfile output -- only print on error 91 l := logger.NewDeferredLogger(ctx) 92 ctx = logger.WithLogger(ctx, l) 93 } else { 94 // send all logs to stderr so stdout has only structured output 95 ctx = logger.WithLogger(ctx, logger.NewLogger(logLvl, c.streams.ErrOut)) 96 } 97 98 deps, err := wireTiltfileResult(ctx, analytics.Get(ctx), "alpha tiltfile-result") 99 if err != nil { 100 c.maybePrintDeferredLogsToStderr(ctx, showTiltfileLogs) 101 return errors.Wrap(err, "wiring dependencies") 102 } 103 104 start := time.Now() 105 tlr := deps.tfl.Load(ctx, ctrltiltfile.MainTiltfile(c.fileName, args), nil) 106 tflDur := time.Since(start) 107 if tlr.Error != nil { 108 c.maybePrintDeferredLogsToStderr(ctx, showTiltfileLogs) 109 110 // Some errors won't JSONify properly by default, so just print it 111 // to STDERR and use the exit code to indicate that it's an error 112 // from Tiltfile parsing. 113 fmt.Fprintln(c.streams.ErrOut, tlr.Error) 114 c.exit(TiltfileErrExitCode) 115 return nil 116 } 117 118 // Instead of printing result JSON, print Builtin Timings instead 119 if c.builtinTimings { 120 if len(tlr.BuiltinCalls) == 0 { 121 return fmt.Errorf("executed Tiltfile, but recorded no Builtin calls") 122 } 123 for _, call := range tlr.BuiltinCalls { 124 if call.Dur < c.durThreshold { 125 continue 126 } 127 argsStr := tupleRE.ReplaceAllString(fmt.Sprintf("%v", call.Args), ")") // clean up tuple stringification 128 fmt.Fprintf(c.streams.Out, "- %s%s took %s\n", call.Name, argsStr, call.Dur) 129 } 130 fmt.Fprintf(c.streams.Out, "Tiltfile execution took %s\n", tflDur.String()) 131 return nil 132 } 133 134 err = encodeJSON(c.streams.Out, tlr) 135 if err != nil { 136 c.maybePrintDeferredLogsToStderr(ctx, showTiltfileLogs) 137 return errors.Wrap(err, "encoding JSON") 138 } 139 return nil 140 } 141 142 func (c *tiltfileResultCmd) maybePrintDeferredLogsToStderr(ctx context.Context, showTiltfileLogs bool) { 143 if showTiltfileLogs { 144 // We've already printed the logs elsewhere, do nothing 145 return 146 } 147 l, ok := logger.Get(ctx).(*logger.DeferredLogger) 148 if !ok { 149 panic(fmt.Sprintf("expected logger of type DeferredLogger, got: %T", logger.Get(ctx))) 150 } 151 stderrLogger := logger.NewLogger(l.Level(), c.streams.ErrOut) 152 l.SetOutput(stderrLogger) 153 }