]> Cypherpunks.ru repositories - goredo.git/blob - buildlog.go
DRY filepath.Abs/Rel
[goredo.git] / buildlog.go
1 /*
2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2023 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         "sort"
29         "strconv"
30         "strings"
31         "time"
32
33         "go.cypherpunks.ru/recfile"
34         "go.cypherpunks.ru/tai64n/v2"
35 )
36
37 const HumanTimeFmt = "2006-01-02 15:04:05.000000000 Z07:00"
38
39 type BuildLogJob struct {
40         dir      string
41         tgt      string
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(dir, tgt string) (map[string][]string, error) {
78         fd, err := os.Open(path.Join(dir, RedoDir, tgt+LogRecSuffix))
79         if err != nil {
80                 return nil, ErrLine(err)
81         }
82         r := recfile.NewReader(bufio.NewReader(fd))
83         rec, err := r.NextMapWithSlice()
84         fd.Close()
85         return rec, ErrLine(err)
86 }
87
88 func depthPrefix(depth int) string {
89         if depth == 0 {
90                 return " "
91         }
92         return " " + colourize(CDebug, strings.Repeat("> ", depth))
93 }
94
95 func showBuildLogSub(sub *BuildLogJob, depth int) error {
96         abs := mustAbs(path.Join(sub.dir, sub.tgt))
97         if _, ok := buildLogSeen[abs]; ok {
98                 return nil
99         }
100         buildLogSeen[abs] = 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),
106         )
107         if err := showBuildLog(sub.dir, 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),
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),
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(dir, tgt string, buildLogRec map[string][]string, depth int) error {
171         var err error
172         dirNormalized, tgtNormalized := cwdAndTgt(path.Join(dir, tgt))
173         if *flagBuildLogCommands || *flagBuildLogRecursive {
174                 buildLogRec, err = parseBuildLogRec(dirNormalized, tgtNormalized)
175                 if err != nil {
176                         return err
177                 }
178         }
179         if *flagBuildLogCommands {
180                 if err = showBuildLogCmd(buildLogRec, depth); err != nil {
181                         return err
182                 }
183         }
184         fd, err := os.Open(path.Join(dirNormalized, RedoDir, tgtNormalized+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 _, dep := range buildLogRec["Ifchange"] {
201                 subDir, subTgt := cwdAndTgt(path.Join(dirNormalized, dep))
202                 if subDir == dirNormalized && subTgt == tgtNormalized {
203                         continue
204                 }
205                 rec, err := parseBuildLogRec(subDir, subTgt)
206                 if err != nil {
207                         if os.IsNotExist(err) {
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                         dir:      dirNormalized,
228                         tgt:      dep,
229                         started:  started,
230                         exitCode: exitCode,
231                         rec:      rec,
232                 })
233         }
234         sort.Sort(ByStarted(subs))
235         scanner := bufio.NewScanner(fd)
236         var text string
237         var sep int
238         var t time.Time
239         var sub *BuildLogJob
240         if len(subs) > 0 {
241                 sub = subs[len(subs)-1]
242         }
243         dp := depthPrefix(depth)
244         for {
245                 if !scanner.Scan() {
246                         if err = scanner.Err(); err != nil {
247                                 return ErrLine(err)
248                         }
249                         break
250                 }
251                 text = scanner.Text()
252                 if text[0] != '@' {
253                         return ErrLine(errors.New("unexpected non-TAI64Ned string"))
254                 }
255                 sep = strings.IndexByte(text, byte(' '))
256                 if sep == -1 {
257                         sep = len(text)
258                 }
259                 t, err = tai64n.Decode(text[1:sep])
260                 if err != nil {
261                         return ErrLine(err)
262                 }
263                 for sub != nil && t.After(sub.started) {
264                         if err = showBuildLogSub(sub, depth); err != nil {
265                                 return err
266                         }
267                         subs = subs[:len(subs)-1]
268                         if len(subs) > 0 {
269                                 sub = subs[len(subs)-1]
270                         } else {
271                                 sub = nil
272                         }
273                 }
274                 if depth == 0 {
275                         fmt.Println(text)
276                 } else {
277                         fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
278                 }
279         }
280         for i := len(subs); i > 0; i-- {
281                 sub = subs[i-1]
282                 if err = showBuildLogSub(sub, depth); err != nil {
283                         return err
284                 }
285         }
286         return nil
287 }