]> Cypherpunks.ru repositories - goredo.git/blob - buildlog.go
Download link for 2.6.2 release
[goredo.git] / buildlog.go
1 // goredo -- djb's redo implementation on pure Go
2 // Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
3 //
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.
7 //
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.
12 //
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/>.
15
16 package main
17
18 import (
19         "bufio"
20         "errors"
21         "flag"
22         "fmt"
23         "io"
24         "io/fs"
25         "os"
26         "path"
27         "sort"
28         "strconv"
29         "strings"
30         "time"
31
32         "go.cypherpunks.ru/recfile"
33         "go.cypherpunks.ru/tai64n/v2"
34 )
35
36 const HumanTimeFmt = "2006-01-02 15:04:05.000000000 Z07:00"
37
38 type BuildLogJob struct {
39         tgt      *Tgt
40         started  time.Time
41         exitCode int
42         rec      map[string][]string
43 }
44
45 type ByStarted []*BuildLogJob
46
47 func (a ByStarted) Len() int { return len(a) }
48
49 func (a ByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
50
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
55                 return true
56         }
57         return a[i].started.After(a[j].started)
58 }
59
60 var (
61         flagBuildLogRecursive *bool
62         flagBuildLogCommands  *bool
63         buildLogSeen          map[string]struct{}
64 )
65
66 func init() {
67         if CmdName() != CmdNameRedoLog {
68                 return
69         }
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{})
73 }
74
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))
78         if err != nil {
79                 return nil, ErrLine(err)
80         }
81         r := recfile.NewReader(bufio.NewReader(fd))
82         rec, err := r.NextMapWithSlice()
83         fd.Close()
84         return rec, ErrLine(err)
85 }
86
87 func depthPrefix(depth int) string {
88         if depth == 0 {
89                 return " "
90         }
91         return " " + colourize(CDebug, strings.Repeat("> ", depth))
92 }
93
94 func showBuildLogSub(sub *BuildLogJob, depth int) error {
95         if _, ok := buildLogSeen[sub.tgt.rel]; ok {
96                 return nil
97         }
98         buildLogSeen[sub.tgt.rel] = struct{}{}
99         dp := depthPrefix(depth)
100         fmt.Printf(
101                 "%s%s%s\n",
102                 sub.rec["Started"][0], dp,
103                 colourize(CRedo, "redo "+sub.tgt.rel),
104         )
105         if err := showBuildLog(sub.tgt, sub.rec, depth+1); err != nil {
106                 return err
107         }
108         durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
109         if err != nil {
110                 return ErrLine(err)
111         }
112         if sub.exitCode > 0 {
113                 fmt.Printf(
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,
118                 )
119         } else {
120                 fmt.Printf(
121                         "%s%s%s (%d.%ds)\n\n",
122                         sub.rec["Finished"][0], dp,
123                         colourize(CRedo, "done "+sub.tgt.rel),
124                         durationSec, durationNsec,
125                 )
126         }
127         return nil
128 }
129
130 func durationToInts(d string) (int64, int64, error) {
131         duration, err := strconv.ParseInt(d, 10, 64)
132         if err != nil {
133                 return 0, 0, err
134         }
135         return duration / 1e9, (duration % 1e9) / 1000, nil
136 }
137
138 func showBuildLogCmd(m map[string][]string, depth int) error {
139         started, err := tai64n.Decode(m["Started"][0])
140         if err != nil {
141                 return ErrLine(err)
142         }
143         dp := depthPrefix(depth)
144         fmt.Printf(
145                 "%s%s%s $ %s\n",
146                 m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "),
147         )
148         if len(m["ExitCode"]) > 0 {
149                 fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0])
150         }
151         finished, err := tai64n.Decode(m["Finished"][0])
152         if err != nil {
153                 return ErrLine(err)
154         }
155         durationSec, durationNsec, err := durationToInts(m["Duration"][0])
156         if err != nil {
157                 return ErrLine(err)
158         }
159         fmt.Printf(
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,
164         )
165         return nil
166 }
167
168 func showBuildLog(tgt *Tgt, buildLogRec map[string][]string, depth int) error {
169         var err error
170         if *flagBuildLogCommands || *flagBuildLogRecursive {
171                 buildLogRec, err = parseBuildLogRec(tgt)
172                 if err != nil {
173                         return err
174                 }
175         }
176         if *flagBuildLogCommands {
177                 if err = showBuildLogCmd(buildLogRec, depth); err != nil {
178                         return err
179                 }
180         }
181         tgtH, tgtT := path.Split(tgt.a)
182         fd, err := os.Open(path.Join(tgtH, RedoDir, tgtT+LogSuffix))
183         if err != nil {
184                 return ErrLine(err)
185         }
186         if !*flagBuildLogRecursive {
187                 w := bufio.NewWriter(os.Stdout)
188                 _, err = io.Copy(w, bufio.NewReader(fd))
189                 fd.Close()
190                 if err != nil {
191                         w.Flush()
192                         return ErrLine(err)
193                 }
194                 return ErrLine(w.Flush())
195         }
196         defer fd.Close()
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 {
201                         continue
202                 }
203                 rec, err := parseBuildLogRec(dep)
204                 if err != nil {
205                         if errors.Is(err, fs.ErrNotExist) {
206                                 continue
207                         }
208                         return err
209                 }
210                 if rec["Build"][0] != buildLogRec["Build"][0] {
211                         continue
212                 }
213                 started, err := tai64n.Decode(rec["Started"][0])
214                 if err != nil {
215                         return ErrLine(err)
216                 }
217                 var exitCode int
218                 if len(rec["ExitCode"]) > 0 {
219                         exitCode, err = strconv.Atoi(rec["ExitCode"][0])
220                         if err != nil {
221                                 return ErrLine(err)
222                         }
223                 }
224                 subs = append(subs, &BuildLogJob{
225                         tgt:      dep,
226                         started:  started,
227                         exitCode: exitCode,
228                         rec:      rec,
229                 })
230         }
231         sort.Sort(ByStarted(subs))
232         scanner := bufio.NewScanner(fd)
233         var text string
234         var sep int
235         var t time.Time
236         var sub *BuildLogJob
237         if len(subs) > 0 {
238                 sub = subs[len(subs)-1]
239         }
240         dp := depthPrefix(depth)
241         for {
242                 if !scanner.Scan() {
243                         if err = scanner.Err(); err != nil {
244                                 return ErrLine(err)
245                         }
246                         break
247                 }
248                 text = scanner.Text()
249                 if text[0] != '@' {
250                         return ErrLine(errors.New("unexpected non-TAI64Ned string"))
251                 }
252                 sep = strings.IndexByte(text, byte(' '))
253                 if sep == -1 {
254                         sep = len(text)
255                 }
256                 t, err = tai64n.Decode(text[1:sep])
257                 if err != nil {
258                         return ErrLine(err)
259                 }
260                 for sub != nil && t.After(sub.started) {
261                         if err = showBuildLogSub(sub, depth); err != nil {
262                                 return err
263                         }
264                         subs = subs[:len(subs)-1]
265                         if len(subs) > 0 {
266                                 sub = subs[len(subs)-1]
267                         } else {
268                                 sub = nil
269                         }
270                 }
271                 if depth == 0 {
272                         fmt.Println(text)
273                 } else {
274                         fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
275                 }
276         }
277         for i := len(subs); i > 0; i-- {
278                 sub = subs[i-1]
279                 if err = showBuildLogSub(sub, depth); err != nil {
280                         return err
281                 }
282         }
283         return nil
284 }