// goredo -- djb's redo implementation on pure Go // Copyright (C) 2020-2024 Sergey Matveev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package main import ( "bufio" "errors" "flag" "fmt" "io" "io/fs" "os" "path" "sort" "strconv" "strings" "time" "go.cypherpunks.ru/recfile" "go.cypherpunks.ru/tai64n/v2" ) const HumanTimeFmt = "2006-01-02 15:04:05.000000000 Z07:00" type BuildLogJob struct { tgt *Tgt started time.Time exitCode int rec map[string][]string } type ByStarted []*BuildLogJob func (a ByStarted) Len() int { return len(a) } func (a ByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByStarted) Less(i, j int) bool { // actually that code performs reverse order if a[i].exitCode > a[j].exitCode { // bad return code has higher priority return true } return a[i].started.After(a[j].started) } var ( flagBuildLogRecursive *bool flagBuildLogCommands *bool buildLogSeen map[string]struct{} ) func init() { if CmdName() != CmdNameRedoLog { return } flagBuildLogRecursive = flag.Bool("r", false, "Show logs recursively") flagBuildLogCommands = flag.Bool("c", false, "Show how target was invoked") buildLogSeen = make(map[string]struct{}) } func parseBuildLogRec(tgt *Tgt) (map[string][]string, error) { h, t := path.Split(tgt.a) fd, err := os.Open(path.Join(h, RedoDir, t+LogRecSuffix)) if err != nil { return nil, ErrLine(err) } r := recfile.NewReader(bufio.NewReader(fd)) rec, err := r.NextMapWithSlice() fd.Close() return rec, ErrLine(err) } func depthPrefix(depth int) string { if depth == 0 { return " " } return " " + colourize(CDebug, strings.Repeat("> ", depth)) } func showBuildLogSub(sub *BuildLogJob, depth int) error { if _, ok := buildLogSeen[sub.tgt.rel]; ok { return nil } buildLogSeen[sub.tgt.rel] = struct{}{} dp := depthPrefix(depth) fmt.Printf( "%s%s%s\n", sub.rec["Started"][0], dp, colourize(CRedo, "redo "+sub.tgt.rel), ) if err := showBuildLog(sub.tgt, sub.rec, depth+1); err != nil { return err } durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0]) if err != nil { return ErrLine(err) } if sub.exitCode > 0 { fmt.Printf( "%s%s%s (code: %d) (%d.%ds)\n\n", sub.rec["Finished"][0], dp, colourize(CErr, "err "+sub.tgt.rel), sub.exitCode, durationSec, durationNsec, ) } else { fmt.Printf( "%s%s%s (%d.%ds)\n\n", sub.rec["Finished"][0], dp, colourize(CRedo, "done "+sub.tgt.rel), durationSec, durationNsec, ) } return nil } func durationToInts(d string) (int64, int64, error) { duration, err := strconv.ParseInt(d, 10, 64) if err != nil { return 0, 0, err } return duration / 1e9, (duration % 1e9) / 1000, nil } func showBuildLogCmd(m map[string][]string, depth int) error { started, err := tai64n.Decode(m["Started"][0]) if err != nil { return ErrLine(err) } dp := depthPrefix(depth) fmt.Printf( "%s%s%s $ %s\n", m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "), ) if len(m["ExitCode"]) > 0 { fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0]) } finished, err := tai64n.Decode(m["Finished"][0]) if err != nil { return ErrLine(err) } durationSec, durationNsec, err := durationToInts(m["Duration"][0]) if err != nil { return ErrLine(err) } fmt.Printf( "%s%sStarted:\t%s\n%s%sFinished:\t%s\n%s%sDuration:\t%d.%ds\n\n", m["Started"][0], dp, started.Format(HumanTimeFmt), m["Started"][0], dp, finished.Format(HumanTimeFmt), m["Started"][0], dp, durationSec, durationNsec, ) return nil } func showBuildLog(tgt *Tgt, buildLogRec map[string][]string, depth int) error { var err error if *flagBuildLogCommands || *flagBuildLogRecursive { buildLogRec, err = parseBuildLogRec(tgt) if err != nil { return err } } if *flagBuildLogCommands { if err = showBuildLogCmd(buildLogRec, depth); err != nil { return err } } tgtH, tgtT := path.Split(tgt.a) fd, err := os.Open(path.Join(tgtH, RedoDir, tgtT+LogSuffix)) if err != nil { return ErrLine(err) } if !*flagBuildLogRecursive { w := bufio.NewWriter(os.Stdout) _, err = io.Copy(w, bufio.NewReader(fd)) fd.Close() if err != nil { w.Flush() return ErrLine(err) } return ErrLine(w.Flush()) } defer fd.Close() subs := make([]*BuildLogJob, 0, len(buildLogRec["Ifchange"])) for _, depPath := range buildLogRec["Ifchange"] { dep := NewTgt(path.Join(tgtH, depPath)) if dep.rel == tgt.rel { continue } rec, err := parseBuildLogRec(dep) if err != nil { if errors.Is(err, fs.ErrNotExist) { continue } return err } if rec["Build"][0] != buildLogRec["Build"][0] { continue } started, err := tai64n.Decode(rec["Started"][0]) if err != nil { return ErrLine(err) } var exitCode int if len(rec["ExitCode"]) > 0 { exitCode, err = strconv.Atoi(rec["ExitCode"][0]) if err != nil { return ErrLine(err) } } subs = append(subs, &BuildLogJob{ tgt: dep, started: started, exitCode: exitCode, rec: rec, }) } sort.Sort(ByStarted(subs)) scanner := bufio.NewScanner(fd) var text string var sep int var t time.Time var sub *BuildLogJob if len(subs) > 0 { sub = subs[len(subs)-1] } dp := depthPrefix(depth) for { if !scanner.Scan() { if err = scanner.Err(); err != nil { return ErrLine(err) } break } text = scanner.Text() if text[0] != '@' { return ErrLine(errors.New("unexpected non-TAI64Ned string")) } sep = strings.IndexByte(text, byte(' ')) if sep == -1 { sep = len(text) } t, err = tai64n.Decode(text[1:sep]) if err != nil { return ErrLine(err) } for sub != nil && t.After(sub.started) { if err = showBuildLogSub(sub, depth); err != nil { return err } subs = subs[:len(subs)-1] if len(subs) > 0 { sub = subs[len(subs)-1] } else { sub = nil } } if depth == 0 { fmt.Println(text) } else { fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:]) } } for i := len(subs); i > 0; i-- { sub = subs[i-1] if err = showBuildLogSub(sub, depth); err != nil { return err } } return nil }