diff --git a/bib/cmd/bootc-image-builder/main.go b/bib/cmd/bootc-image-builder/main.go index 6eb0b6340..2c88d203a 100644 --- a/bib/cmd/bootc-image-builder/main.go +++ b/bib/cmd/bootc-image-builder/main.go @@ -26,6 +26,7 @@ import ( "github.com/osbuild/bootc-image-builder/bib/internal/buildconfig" podman_container "github.com/osbuild/bootc-image-builder/bib/internal/container" + "github.com/osbuild/bootc-image-builder/bib/internal/osbuildprogress" "github.com/osbuild/bootc-image-builder/bib/internal/setup" "github.com/osbuild/bootc-image-builder/bib/internal/source" "github.com/osbuild/bootc-image-builder/bib/internal/util" @@ -381,6 +382,7 @@ func cmdBuild(cmd *cobra.Command, args []string) error { osbuildStore, _ := cmd.Flags().GetString("store") outputDir, _ := cmd.Flags().GetString("output") targetArch, _ := cmd.Flags().GetString("target-arch") + progress, _ := cmd.Flags().GetString("progress") logrus.Debug("Validating environment") if err := setup.Validate(targetArch); err != nil { @@ -463,7 +465,12 @@ func cmdBuild(cmd *cobra.Command, args []string) error { osbuildEnv = append(osbuildEnv, envVars...) } - _, err = osbuild.RunOSBuild(mf, osbuildStore, outputDir, exports, nil, osbuildEnv, false, os.Stderr) + switch progress { + case "text": + err = osbuildprogress.RunOSBuild(mf, osbuildStore, outputDir, exports, osbuildEnv) + default: + _, err = osbuild.RunOSBuild(mf, osbuildStore, outputDir, exports, nil, osbuildEnv, false, os.Stderr) + } if err != nil { return fmt.Errorf("cannot run osbuild: %w", err) } @@ -613,7 +620,8 @@ func run() error { buildCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)") buildCmd.Flags().String("chown", "", "chown the ouput directory to match the specified UID:GID") buildCmd.Flags().String("output", ".", "artifact output directory") - buildCmd.Flags().String("progress", "text", "type of progress bar to use") + // XXX: make this a proper type + buildCmd.Flags().String("progress", "none", "type of progress bar to use") buildCmd.Flags().String("store", "/store", "osbuild store for intermediate pipeline trees") // flag rules diff --git a/bib/internal/osbuildprogress/progress.go b/bib/internal/osbuildprogress/progress.go new file mode 100644 index 000000000..6988598c3 --- /dev/null +++ b/bib/internal/osbuildprogress/progress.go @@ -0,0 +1,176 @@ +package osbuildprogress + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" +) + +type OsbuildJsonProgress struct { + ID string `json:"id"` + Context struct { + Origin string `json:"origin"` + Pipeline struct { + Name string `json:"name"` + Stage struct { + Name string `json:"name"` + ID string `json:"id"` + } `json:"stage"` + ID string `json:"id"` + } `json:"pipeline"` + } `json:"context"` + Progress struct { + Name string `json:"name"` + Total int `json:"total"` + Done int `json:"done"` + // XXX: there are currently only two levels but it should be + // deeper nested in theory + SubProgress struct { + Name string `json:"name"` + Total int `json:"total"` + Done int `json:"done"` + // XXX: in theory this could be more nested but it's not + + } `json:"progress"` + } `json:"progress"` + + Message string `json:"message"` +} + +func scanJsonSeq(r io.Reader, ch chan OsbuildJsonProgress) { + var progress OsbuildJsonProgress + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + // XXX: use a proper jsonseq reader? + line := scanner.Bytes() + line = bytes.Trim(line, "\x1e") + if err := json.Unmarshal(line, &progress); err != nil { + // XXX: provide an invalid lines chan + fmt.Fprintf(os.Stderr, "json decode err for line %q: %v\n", line, err) + continue + } + ch <- progress + } + if err := scanner.Err(); err != nil && err != io.EOF { + // log error here + } +} + +func AttachProgress(r io.Reader, w io.Writer) { + var progress OsbuildJsonProgress + spinner := []string{"|", "/", "-", "\\"} + i := 0 + + ch := make(chan OsbuildJsonProgress) + go scanJsonSeq(r, ch) + + mainProgress := "unknown" + subProgress := "" + message := "-" + + contextMap := map[string]string{} + + fmt.Fprintf(w, "\n") + for { + select { + case progress = <-ch: + id := progress.Context.Pipeline.ID + pipelineName := contextMap[id] + if pipelineName == "" { + pipelineName = progress.Context.Pipeline.Name + contextMap[id] = pipelineName + } + // XXX: use differentmap? + id = "stage-" + progress.Context.Pipeline.Stage.ID + stageName := contextMap[id] + if stageName == "" { + stageName = progress.Context.Pipeline.Stage.Name + contextMap[id] = stageName + } + + if progress.Progress.Total > 0 { + mainProgress = fmt.Sprintf("step: %v [%v/%v]", pipelineName, progress.Progress.Done+1, progress.Progress.Total+1) + } + // XXX: use context instead of name here too + if progress.Progress.SubProgress.Total > 0 { + subProgress = fmt.Sprintf("%v [%v/%v]", stageName, progress.Progress.SubProgress.Done+1, progress.Progress.SubProgress.Total+1) + } + + // todo: make message more structured in osbuild? + // message from the stages themselfs are very noisy + // best not to show to the user (only for failures) + if progress.Context.Origin == "osbuild.monitor" { + message = progress.Message + } + //message = strings.TrimSpace(strings.SplitN(progress.Message, "\n", 2)[0]) + // todo: fix in osbuild? + /* + l := strings.SplitN(message, ":", 2) + if len(l) > 1 { + message = strings.TrimSpace(l[1]) + } + */ + if len(message) > 60 { + message = message[:60] + "..." + } + case <-time.After(200 * time.Millisecond): + // nothing + } + + // XXX: use real progressbar *or* use helper to get terminal + // size for proper length checks etc + // + // poor man progress, we need multiple progress bars and + // a message that keeps getting updated (or maybe not the + // message) + fmt.Fprintf(w, "\x1b[2KBuilding [%s]\n", spinner[i]) + fmt.Fprintf(w, "\x1b[2Kstep : %s\n", mainProgress) + if subProgress != "" { + fmt.Fprintf(w, "\x1b[2Kmodule : %s\n", strings.TrimPrefix(subProgress, "org.osbuild.")) + } + fmt.Fprintf(w, "\x1b[3Kmessage: %s\n", message) + if subProgress != "" { + fmt.Fprintf(w, "\x1b[%dA", 4) + } else { + fmt.Fprintf(w, "\x1b[%dA", 3) + } + // spin + i = (i + 1) % len(spinner) + } +} + +// XXX: merge back into images/pkg/osbuild/osbuild-exec.go(?) +func RunOSBuild(manifest []byte, store, outputDirectory string, exports, extraEnv []string) error { + cmd := exec.Command( + "osbuild", + "--store", store, + "--output-directory", outputDirectory, + "--monitor=JSONSeqMonitor", + "-", + ) + cmd.Env = append(os.Environ(), extraEnv...) + cmd.Stdin = bytes.NewBuffer(manifest) + cmd.Stderr = os.Stderr + + for _, export := range exports { + cmd.Args = append(cmd.Args, "--export", export) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + go AttachProgress(stdout, os.Stdout) + + if err := cmd.Start(); err != nil { + return fmt.Errorf("error starting osbuild: %v", err) + } + return cmd.Wait() +}