]> Cypherpunks.ru repositories - goredo.git/blobdiff - buildlog.go
Recursive serialized logs capability
[goredo.git] / buildlog.go
diff --git a/buildlog.go b/buildlog.go
new file mode 100644 (file)
index 0000000..3403e4d
--- /dev/null
@@ -0,0 +1,300 @@
+/*
+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
+}