+/*
+goredo -- djb's redo implementation on pure Go
+Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+package main
+
+import (
+ "bufio"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "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 {
+ dir string
+ tgt string
+ 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(dir, tgt string) (map[string][]string, error) {
+ fd, err := os.Open(path.Join(dir, RedoDir, tgt+LogSuffix+DepSuffix))
+ if err != nil {
+ return nil, err
+ }
+ r := recfile.NewReader(bufio.NewReader(fd))
+ rec, err := r.NextMapWithSlice()
+ fd.Close()
+ return rec, err
+}
+
+func depthPrefix(depth int) string {
+ if depth == 0 {
+ return " "
+ }
+ return " " + colourize(CDebug, strings.Repeat("> ", depth))
+}
+
+func showBuildLogSub(sub *BuildLogJob, depth int) error {
+ abs, err := filepath.Abs(path.Join(sub.dir, sub.tgt))
+ if err != nil {
+ return err
+ }
+ if _, ok := buildLogSeen[abs]; ok {
+ return nil
+ }
+ buildLogSeen[abs] = struct{}{}
+ dp := depthPrefix(depth)
+ fmt.Printf(
+ "%s%s%s\n",
+ sub.rec["Started"][0], dp,
+ colourize(CRedo, "redo "+sub.tgt),
+ )
+ if err := showBuildLog(sub.dir, sub.tgt, sub.rec, depth+1); err != nil {
+ return err
+ }
+ durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
+ if err != nil {
+ return 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),
+ sub.exitCode, durationSec, durationNsec,
+ )
+ } else {
+ fmt.Printf(
+ "%s%s%s (%d.%ds)\n\n",
+ sub.rec["Finished"][0], dp,
+ colourize(CRedo, "done "+sub.tgt),
+ 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 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 err
+ }
+ durationSec, durationNsec, err := durationToInts(m["Duration"][0])
+ if err != nil {
+ return 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(dir, tgt string, buildLogRec map[string][]string, depth int) error {
+ var err error
+ dirNormalized, tgtNormalized := cwdAndTgt(path.Join(dir, tgt))
+ if *flagBuildLogCommands || *flagBuildLogRecursive {
+ buildLogRec, err = parseBuildLogRec(dirNormalized, tgtNormalized)
+ if err != nil {
+ return err
+ }
+ }
+ if *flagBuildLogCommands {
+ if err = showBuildLogCmd(buildLogRec, depth); err != nil {
+ return err
+ }
+ }
+ fd, err := os.Open(path.Join(dirNormalized, RedoDir, tgtNormalized+LogSuffix))
+ if err != nil {
+ return err
+ }
+ if !*flagBuildLogRecursive {
+ w := bufio.NewWriter(os.Stdout)
+ _, err = io.Copy(w, bufio.NewReader(fd))
+ fd.Close()
+ if err != nil {
+ w.Flush()
+ return err
+ }
+ return w.Flush()
+ }
+ defer fd.Close()
+ fdDep, err := os.Open(path.Join(dirNormalized, RedoDir, tgtNormalized+DepSuffix))
+ if err != nil {
+ return err
+ }
+ depInfo, err := depRead(fdDep)
+ fdDep.Close()
+ if err != nil {
+ return err
+ }
+ subs := make([]*BuildLogJob, 0, len(depInfo.ifchanges))
+ for _, dep := range depInfo.ifchanges {
+ subDir, subTgt := cwdAndTgt(path.Join(dirNormalized, dep["Target"]))
+ if subDir == dirNormalized && subTgt == tgtNormalized {
+ continue
+ }
+ rec, err := parseBuildLogRec(subDir, subTgt)
+ if err != nil {
+ if os.IsNotExist(err) {
+ continue
+ }
+ return err
+ }
+ if rec["Build"][0] != buildLogRec["Build"][0] {
+ continue
+ }
+ started, err := tai64n.Decode(rec["Started"][0])
+ if err != nil {
+ return err
+ }
+ var exitCode int
+ if len(rec["ExitCode"]) > 0 {
+ exitCode, err = strconv.Atoi(rec["ExitCode"][0])
+ if err != nil {
+ return err
+ }
+ }
+ subs = append(subs, &BuildLogJob{
+ dir: dirNormalized,
+ tgt: dep["Target"],
+ 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 err
+ }
+ break
+ }
+ text = scanner.Text()
+ if text[0] != '@' {
+ return 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 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
+}