]> Cypherpunks.ru repositories - goredo.git/blob - buildlog.go
Recursive serialized logs capability
[goredo.git] / buildlog.go
1 /*
2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2021 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         "os"
27         "path"
28         "path/filepath"
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         dir      string
42         tgt      string
43         started  time.Time
44         exitCode int
45         rec      map[string][]string
46 }
47
48 type ByStarted []*BuildLogJob
49
50 func (a ByStarted) Len() int { return len(a) }
51
52 func (a ByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
53
54 func (a ByStarted) Less(i, j int) bool {
55         // actually that code performs reverse order
56         if a[i].exitCode > a[j].exitCode {
57                 // bad return code has higher priority
58                 return true
59         }
60         return a[i].started.After(a[j].started)
61 }
62
63 var (
64         flagBuildLogRecursive *bool
65         flagBuildLogCommands  *bool
66         buildLogSeen          map[string]struct{}
67 )
68
69 func init() {
70         if CmdName() != CmdNameRedoLog {
71                 return
72         }
73         flagBuildLogRecursive = flag.Bool("r", false, "Show logs recursively")
74         flagBuildLogCommands = flag.Bool("c", false, "Show how target was invoked")
75         buildLogSeen = make(map[string]struct{})
76 }
77
78 func parseBuildLogRec(dir, tgt string) (map[string][]string, error) {
79         fd, err := os.Open(path.Join(dir, RedoDir, tgt+LogSuffix+DepSuffix))
80         if err != nil {
81                 return nil, err
82         }
83         r := recfile.NewReader(bufio.NewReader(fd))
84         rec, err := r.NextMapWithSlice()
85         fd.Close()
86         return rec, 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         abs, err := filepath.Abs(path.Join(sub.dir, sub.tgt))
98         if err != nil {
99                 return err
100         }
101         if _, ok := buildLogSeen[abs]; ok {
102                 return nil
103         }
104         buildLogSeen[abs] = struct{}{}
105         dp := depthPrefix(depth)
106         fmt.Printf(
107                 "%s%s%s\n",
108                 sub.rec["Started"][0], dp,
109                 colourize(CRedo, "redo "+sub.tgt),
110         )
111         if err := showBuildLog(sub.dir, sub.tgt, sub.rec, depth+1); err != nil {
112                 return err
113         }
114         durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
115         if err != nil {
116                 return err
117         }
118         if sub.exitCode > 0 {
119                 fmt.Printf(
120                         "%s%s%s (code: %d) (%d.%ds)\n\n",
121                         sub.rec["Finished"][0], dp,
122                         colourize(CErr, "err "+sub.tgt),
123                         sub.exitCode, durationSec, durationNsec,
124                 )
125         } else {
126                 fmt.Printf(
127                         "%s%s%s (%d.%ds)\n\n",
128                         sub.rec["Finished"][0], dp,
129                         colourize(CRedo, "done "+sub.tgt),
130                         durationSec, durationNsec,
131                 )
132         }
133         return nil
134 }
135
136 func durationToInts(d string) (int64, int64, error) {
137         duration, err := strconv.ParseInt(d, 10, 64)
138         if err != nil {
139                 return 0, 0, err
140         }
141         return duration / 1e9, (duration % 1e9) / 1000, nil
142 }
143
144 func showBuildLogCmd(m map[string][]string, depth int) error {
145         started, err := tai64n.Decode(m["Started"][0])
146         if err != nil {
147                 return err
148         }
149         dp := depthPrefix(depth)
150         fmt.Printf(
151                 "%s%s%s $ %s\n",
152                 m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "),
153         )
154         if len(m["ExitCode"]) > 0 {
155                 fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0])
156         }
157         finished, err := tai64n.Decode(m["Finished"][0])
158         if err != nil {
159                 return err
160         }
161         durationSec, durationNsec, err := durationToInts(m["Duration"][0])
162         if err != nil {
163                 return err
164         }
165         fmt.Printf(
166                 "%s%sStarted:\t%s\n%s%sFinished:\t%s\n%s%sDuration:\t%d.%ds\n\n",
167                 m["Started"][0], dp, started.Format(HumanTimeFmt),
168                 m["Started"][0], dp, finished.Format(HumanTimeFmt),
169                 m["Started"][0], dp, durationSec, durationNsec,
170         )
171         return nil
172 }
173
174 func showBuildLog(dir, tgt string, buildLogRec map[string][]string, depth int) error {
175         var err error
176         dirNormalized, tgtNormalized := cwdAndTgt(path.Join(dir, tgt))
177         if *flagBuildLogCommands || *flagBuildLogRecursive {
178                 buildLogRec, err = parseBuildLogRec(dirNormalized, tgtNormalized)
179                 if err != nil {
180                         return err
181                 }
182         }
183         if *flagBuildLogCommands {
184                 if err = showBuildLogCmd(buildLogRec, depth); err != nil {
185                         return err
186                 }
187         }
188         fd, err := os.Open(path.Join(dirNormalized, RedoDir, tgtNormalized+LogSuffix))
189         if err != nil {
190                 return err
191         }
192         if !*flagBuildLogRecursive {
193                 w := bufio.NewWriter(os.Stdout)
194                 _, err = io.Copy(w, bufio.NewReader(fd))
195                 fd.Close()
196                 if err != nil {
197                         w.Flush()
198                         return err
199                 }
200                 return w.Flush()
201         }
202         defer fd.Close()
203         fdDep, err := os.Open(path.Join(dirNormalized, RedoDir, tgtNormalized+DepSuffix))
204         if err != nil {
205                 return err
206         }
207         depInfo, err := depRead(fdDep)
208         fdDep.Close()
209         if err != nil {
210                 return err
211         }
212         subs := make([]*BuildLogJob, 0, len(depInfo.ifchanges))
213         for _, dep := range depInfo.ifchanges {
214                 subDir, subTgt := cwdAndTgt(path.Join(dirNormalized, dep["Target"]))
215                 if subDir == dirNormalized && subTgt == tgtNormalized {
216                         continue
217                 }
218                 rec, err := parseBuildLogRec(subDir, subTgt)
219                 if err != nil {
220                         if os.IsNotExist(err) {
221                                 continue
222                         }
223                         return err
224                 }
225                 if rec["Build"][0] != buildLogRec["Build"][0] {
226                         continue
227                 }
228                 started, err := tai64n.Decode(rec["Started"][0])
229                 if err != nil {
230                         return err
231                 }
232                 var exitCode int
233                 if len(rec["ExitCode"]) > 0 {
234                         exitCode, err = strconv.Atoi(rec["ExitCode"][0])
235                         if err != nil {
236                                 return err
237                         }
238                 }
239                 subs = append(subs, &BuildLogJob{
240                         dir:      dirNormalized,
241                         tgt:      dep["Target"],
242                         started:  started,
243                         exitCode: exitCode,
244                         rec:      rec,
245                 })
246         }
247         sort.Sort(ByStarted(subs))
248         scanner := bufio.NewScanner(fd)
249         var text string
250         var sep int
251         var t time.Time
252         var sub *BuildLogJob
253         if len(subs) > 0 {
254                 sub = subs[len(subs)-1]
255         }
256         dp := depthPrefix(depth)
257         for {
258                 if !scanner.Scan() {
259                         if err = scanner.Err(); err != nil {
260                                 return err
261                         }
262                         break
263                 }
264                 text = scanner.Text()
265                 if text[0] != '@' {
266                         return errors.New("unexpected non-TAI64Ned string")
267                 }
268                 sep = strings.IndexByte(text, byte(' '))
269                 if sep == -1 {
270                         sep = len(text)
271                 }
272                 t, err = tai64n.Decode(text[1:sep])
273                 if err != nil {
274                         return err
275                 }
276                 for sub != nil && t.After(sub.started) {
277                         if err = showBuildLogSub(sub, depth); err != nil {
278                                 return err
279                         }
280                         subs = subs[:len(subs)-1]
281                         if len(subs) > 0 {
282                                 sub = subs[len(subs)-1]
283                         } else {
284                                 sub = nil
285                         }
286                 }
287                 if depth == 0 {
288                         fmt.Println(text)
289                 } else {
290                         fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
291                 }
292         }
293         for i := len(subs); i > 0; i-- {
294                 sub = subs[i-1]
295                 if err = showBuildLogSub(sub, depth); err != nil {
296                         return err
297                 }
298         }
299         return nil
300 }