1 // goredo -- djb's redo implementation on pure Go
2 // Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, version 3 of the License.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
32 "go.cypherpunks.ru/recfile"
33 "go.cypherpunks.ru/tai64n/v2"
36 const HumanTimeFmt = "2006-01-02 15:04:05.000000000 Z07:00"
38 type BuildLogJob struct {
42 rec map[string][]string
45 type ByStarted []*BuildLogJob
47 func (a ByStarted) Len() int { return len(a) }
49 func (a ByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
51 func (a ByStarted) Less(i, j int) bool {
52 // actually that code performs reverse order
53 if a[i].exitCode > a[j].exitCode {
54 // bad return code has higher priority
57 return a[i].started.After(a[j].started)
61 flagBuildLogRecursive *bool
62 flagBuildLogCommands *bool
63 buildLogSeen map[string]struct{}
67 if CmdName() != CmdNameRedoLog {
70 flagBuildLogRecursive = flag.Bool("r", false, "Show logs recursively")
71 flagBuildLogCommands = flag.Bool("c", false, "Show how target was invoked")
72 buildLogSeen = make(map[string]struct{})
75 func parseBuildLogRec(tgt *Tgt) (map[string][]string, error) {
76 h, t := path.Split(tgt.a)
77 fd, err := os.Open(path.Join(h, RedoDir, t+LogRecSuffix))
79 return nil, ErrLine(err)
81 r := recfile.NewReader(bufio.NewReader(fd))
82 rec, err := r.NextMapWithSlice()
84 return rec, ErrLine(err)
87 func depthPrefix(depth int) string {
91 return " " + colourize(CDebug, strings.Repeat("> ", depth))
94 func showBuildLogSub(sub *BuildLogJob, depth int) error {
95 if _, ok := buildLogSeen[sub.tgt.rel]; ok {
98 buildLogSeen[sub.tgt.rel] = struct{}{}
99 dp := depthPrefix(depth)
102 sub.rec["Started"][0], dp,
103 colourize(CRedo, "redo "+sub.tgt.rel),
105 if err := showBuildLog(sub.tgt, sub.rec, depth+1); err != nil {
108 durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
112 if sub.exitCode > 0 {
114 "%s%s%s (code: %d) (%d.%ds)\n\n",
115 sub.rec["Finished"][0], dp,
116 colourize(CErr, "err "+sub.tgt.rel),
117 sub.exitCode, durationSec, durationNsec,
121 "%s%s%s (%d.%ds)\n\n",
122 sub.rec["Finished"][0], dp,
123 colourize(CRedo, "done "+sub.tgt.rel),
124 durationSec, durationNsec,
130 func durationToInts(d string) (int64, int64, error) {
131 duration, err := strconv.ParseInt(d, 10, 64)
135 return duration / 1e9, (duration % 1e9) / 1000, nil
138 func showBuildLogCmd(m map[string][]string, depth int) error {
139 started, err := tai64n.Decode(m["Started"][0])
143 dp := depthPrefix(depth)
146 m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "),
148 if len(m["ExitCode"]) > 0 {
149 fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0])
151 finished, err := tai64n.Decode(m["Finished"][0])
155 durationSec, durationNsec, err := durationToInts(m["Duration"][0])
160 "%s%sStarted:\t%s\n%s%sFinished:\t%s\n%s%sDuration:\t%d.%ds\n\n",
161 m["Started"][0], dp, started.Format(HumanTimeFmt),
162 m["Started"][0], dp, finished.Format(HumanTimeFmt),
163 m["Started"][0], dp, durationSec, durationNsec,
168 func showBuildLog(tgt *Tgt, buildLogRec map[string][]string, depth int) error {
170 if *flagBuildLogCommands || *flagBuildLogRecursive {
171 buildLogRec, err = parseBuildLogRec(tgt)
176 if *flagBuildLogCommands {
177 if err = showBuildLogCmd(buildLogRec, depth); err != nil {
181 tgtH, tgtT := path.Split(tgt.a)
182 fd, err := os.Open(path.Join(tgtH, RedoDir, tgtT+LogSuffix))
186 if !*flagBuildLogRecursive {
187 w := bufio.NewWriter(os.Stdout)
188 _, err = io.Copy(w, bufio.NewReader(fd))
194 return ErrLine(w.Flush())
197 subs := make([]*BuildLogJob, 0, len(buildLogRec["Ifchange"]))
198 for _, depPath := range buildLogRec["Ifchange"] {
199 dep := NewTgt(path.Join(tgtH, depPath))
200 if dep.rel == tgt.rel {
203 rec, err := parseBuildLogRec(dep)
205 if errors.Is(err, fs.ErrNotExist) {
210 if rec["Build"][0] != buildLogRec["Build"][0] {
213 started, err := tai64n.Decode(rec["Started"][0])
218 if len(rec["ExitCode"]) > 0 {
219 exitCode, err = strconv.Atoi(rec["ExitCode"][0])
224 subs = append(subs, &BuildLogJob{
231 sort.Sort(ByStarted(subs))
232 scanner := bufio.NewScanner(fd)
238 sub = subs[len(subs)-1]
240 dp := depthPrefix(depth)
243 if err = scanner.Err(); err != nil {
248 text = scanner.Text()
250 return ErrLine(errors.New("unexpected non-TAI64Ned string"))
252 sep = strings.IndexByte(text, byte(' '))
256 t, err = tai64n.Decode(text[1:sep])
260 for sub != nil && t.After(sub.started) {
261 if err = showBuildLogSub(sub, depth); err != nil {
264 subs = subs[:len(subs)-1]
266 sub = subs[len(subs)-1]
274 fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
277 for i := len(subs); i > 0; i-- {
279 if err = showBuildLogSub(sub, depth); err != nil {