]> Cypherpunks.ru repositories - goredo.git/blob - buildlog.go
fcaf3704524e15cc5f44ed47c5d29e2269ecbe09
[goredo.git] / buildlog.go
1 /*
2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package main
19
20 import (
21         "bufio"
22         "errors"
23         "flag"
24         "fmt"
25         "io"
26         "io/fs"
27         "os"
28         "path"
29         "sort"
30         "strconv"
31         "strings"
32         "time"
33
34         "go.cypherpunks.ru/recfile"
35         "go.cypherpunks.ru/tai64n/v2"
36 )
37
38 const HumanTimeFmt = "2006-01-02 15:04:05.000000000 Z07:00"
39
40 type BuildLogJob struct {
41         tgt      *Tgt
42         started  time.Time
43         exitCode int
44         rec      map[string][]string
45 }
46
47 type ByStarted []*BuildLogJob
48
49 func (a ByStarted) Len() int { return len(a) }
50
51 func (a ByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
52
53 func (a ByStarted) Less(i, j int) bool {
54         // actually that code performs reverse order
55         if a[i].exitCode > a[j].exitCode {
56                 // bad return code has higher priority
57                 return true
58         }
59         return a[i].started.After(a[j].started)
60 }
61
62 var (
63         flagBuildLogRecursive *bool
64         flagBuildLogCommands  *bool
65         buildLogSeen          map[string]struct{}
66 )
67
68 func init() {
69         if CmdName() != CmdNameRedoLog {
70                 return
71         }
72         flagBuildLogRecursive = flag.Bool("r", false, "Show logs recursively")
73         flagBuildLogCommands = flag.Bool("c", false, "Show how target was invoked")
74         buildLogSeen = make(map[string]struct{})
75 }
76
77 func parseBuildLogRec(tgt *Tgt) (map[string][]string, error) {
78         h, t := path.Split(tgt.a)
79         fd, err := os.Open(path.Join(h, RedoDir, t+LogRecSuffix))
80         if err != nil {
81                 return nil, ErrLine(err)
82         }
83         r := recfile.NewReader(bufio.NewReader(fd))
84         rec, err := r.NextMapWithSlice()
85         fd.Close()
86         return rec, ErrLine(err)
87 }
88
89 func depthPrefix(depth int) string {
90         if depth == 0 {
91                 return " "
92         }
93         return " " + colourize(CDebug, strings.Repeat("> ", depth))
94 }
95
96 func showBuildLogSub(sub *BuildLogJob, depth int) error {
97         if _, ok := buildLogSeen[sub.tgt.rel]; ok {
98                 return nil
99         }
100         buildLogSeen[sub.tgt.rel] = struct{}{}
101         dp := depthPrefix(depth)
102         fmt.Printf(
103                 "%s%s%s\n",
104                 sub.rec["Started"][0], dp,
105                 colourize(CRedo, "redo "+sub.tgt.rel),
106         )
107         if err := showBuildLog(sub.tgt, sub.rec, depth+1); err != nil {
108                 return err
109         }
110         durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
111         if err != nil {
112                 return ErrLine(err)
113         }
114         if sub.exitCode > 0 {
115                 fmt.Printf(
116                         "%s%s%s (code: %d) (%d.%ds)\n\n",
117                         sub.rec["Finished"][0], dp,
118                         colourize(CErr, "err "+sub.tgt.rel),
119                         sub.exitCode, durationSec, durationNsec,
120                 )
121         } else {
122                 fmt.Printf(
123                         "%s%s%s (%d.%ds)\n\n",
124                         sub.rec["Finished"][0], dp,
125                         colourize(CRedo, "done "+sub.tgt.rel),
126                         durationSec, durationNsec,
127                 )
128         }
129         return nil
130 }
131
132 func durationToInts(d string) (int64, int64, error) {
133         duration, err := strconv.ParseInt(d, 10, 64)
134         if err != nil {
135                 return 0, 0, err
136         }
137         return duration / 1e9, (duration % 1e9) / 1000, nil
138 }
139
140 func showBuildLogCmd(m map[string][]string, depth int) error {
141         started, err := tai64n.Decode(m["Started"][0])
142         if err != nil {
143                 return ErrLine(err)
144         }
145         dp := depthPrefix(depth)
146         fmt.Printf(
147                 "%s%s%s $ %s\n",
148                 m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "),
149         )
150         if len(m["ExitCode"]) > 0 {
151                 fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0])
152         }
153         finished, err := tai64n.Decode(m["Finished"][0])
154         if err != nil {
155                 return ErrLine(err)
156         }
157         durationSec, durationNsec, err := durationToInts(m["Duration"][0])
158         if err != nil {
159                 return ErrLine(err)
160         }
161         fmt.Printf(
162                 "%s%sStarted:\t%s\n%s%sFinished:\t%s\n%s%sDuration:\t%d.%ds\n\n",
163                 m["Started"][0], dp, started.Format(HumanTimeFmt),
164                 m["Started"][0], dp, finished.Format(HumanTimeFmt),
165                 m["Started"][0], dp, durationSec, durationNsec,
166         )
167         return nil
168 }
169
170 func showBuildLog(tgt *Tgt, buildLogRec map[string][]string, depth int) error {
171         var err error
172         if *flagBuildLogCommands || *flagBuildLogRecursive {
173                 buildLogRec, err = parseBuildLogRec(tgt)
174                 if err != nil {
175                         return err
176                 }
177         }
178         if *flagBuildLogCommands {
179                 if err = showBuildLogCmd(buildLogRec, depth); err != nil {
180                         return err
181                 }
182         }
183         tgtH, tgtT := path.Split(tgt.a)
184         fd, err := os.Open(path.Join(tgtH, RedoDir, tgtT+LogSuffix))
185         if err != nil {
186                 return ErrLine(err)
187         }
188         if !*flagBuildLogRecursive {
189                 w := bufio.NewWriter(os.Stdout)
190                 _, err = io.Copy(w, bufio.NewReader(fd))
191                 fd.Close()
192                 if err != nil {
193                         w.Flush()
194                         return ErrLine(err)
195                 }
196                 return ErrLine(w.Flush())
197         }
198         defer fd.Close()
199         subs := make([]*BuildLogJob, 0, len(buildLogRec["Ifchange"]))
200         for _, depPath := range buildLogRec["Ifchange"] {
201                 dep := NewTgt(path.Join(tgtH, depPath))
202                 if dep.rel == tgt.rel {
203                         continue
204                 }
205                 rec, err := parseBuildLogRec(dep)
206                 if err != nil {
207                         if errors.Is(err, fs.ErrNotExist) {
208                                 continue
209                         }
210                         return err
211                 }
212                 if rec["Build"][0] != buildLogRec["Build"][0] {
213                         continue
214                 }
215                 started, err := tai64n.Decode(rec["Started"][0])
216                 if err != nil {
217                         return ErrLine(err)
218                 }
219                 var exitCode int
220                 if len(rec["ExitCode"]) > 0 {
221                         exitCode, err = strconv.Atoi(rec["ExitCode"][0])
222                         if err != nil {
223                                 return ErrLine(err)
224                         }
225                 }
226                 subs = append(subs, &BuildLogJob{
227                         tgt:      dep,
228                         started:  started,
229                         exitCode: exitCode,
230                         rec:      rec,
231                 })
232         }
233         sort.Sort(ByStarted(subs))
234         scanner := bufio.NewScanner(fd)
235         var text string
236         var sep int
237         var t time.Time
238         var sub *BuildLogJob
239         if len(subs) > 0 {
240                 sub = subs[len(subs)-1]
241         }
242         dp := depthPrefix(depth)
243         for {
244                 if !scanner.Scan() {
245                         if err = scanner.Err(); err != nil {
246                                 return ErrLine(err)
247                         }
248                         break
249                 }
250                 text = scanner.Text()
251                 if text[0] != '@' {
252                         return ErrLine(errors.New("unexpected non-TAI64Ned string"))
253                 }
254                 sep = strings.IndexByte(text, byte(' '))
255                 if sep == -1 {
256                         sep = len(text)
257                 }
258                 t, err = tai64n.Decode(text[1:sep])
259                 if err != nil {
260                         return ErrLine(err)
261                 }
262                 for sub != nil && t.After(sub.started) {
263                         if err = showBuildLogSub(sub, depth); err != nil {
264                                 return err
265                         }
266                         subs = subs[:len(subs)-1]
267                         if len(subs) > 0 {
268                                 sub = subs[len(subs)-1]
269                         } else {
270                                 sub = nil
271                         }
272                 }
273                 if depth == 0 {
274                         fmt.Println(text)
275                 } else {
276                         fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
277                 }
278         }
279         for i := len(subs); i > 0; i-- {
280                 sub = subs[i-1]
281                 if err = showBuildLogSub(sub, depth); err != nil {
282                         return err
283                 }
284         }
285         return nil
286 }