]> Cypherpunks.ru repositories - goredo.git/blob - buildlog.go
6c5793774beb489a36a14a33ca3c1c8582fb9797
[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         "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         fd, err := os.Open(path.Join(tgt.h, RedoDir, tgt.t+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         if _, ok := buildLogSeen[sub.tgt.rel]; ok {
97                 return nil
98         }
99         buildLogSeen[sub.tgt.rel] = struct{}{}
100         dp := depthPrefix(depth)
101         fmt.Printf(
102                 "%s%s%s\n",
103                 sub.rec["Started"][0], dp,
104                 colourize(CRedo, "redo "+sub.tgt.rel),
105         )
106         if err := showBuildLog(sub.tgt, sub.rec, depth+1); err != nil {
107                 return err
108         }
109         durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
110         if err != nil {
111                 return ErrLine(err)
112         }
113         if sub.exitCode > 0 {
114                 fmt.Printf(
115                         "%s%s%s (code: %d) (%d.%ds)\n\n",
116                         sub.rec["Finished"][0], dp,
117                         colourize(CErr, "err "+sub.tgt.rel),
118                         sub.exitCode, durationSec, durationNsec,
119                 )
120         } else {
121                 fmt.Printf(
122                         "%s%s%s (%d.%ds)\n\n",
123                         sub.rec["Finished"][0], dp,
124                         colourize(CRedo, "done "+sub.tgt.rel),
125                         durationSec, durationNsec,
126                 )
127         }
128         return nil
129 }
130
131 func durationToInts(d string) (int64, int64, error) {
132         duration, err := strconv.ParseInt(d, 10, 64)
133         if err != nil {
134                 return 0, 0, err
135         }
136         return duration / 1e9, (duration % 1e9) / 1000, nil
137 }
138
139 func showBuildLogCmd(m map[string][]string, depth int) error {
140         started, err := tai64n.Decode(m["Started"][0])
141         if err != nil {
142                 return ErrLine(err)
143         }
144         dp := depthPrefix(depth)
145         fmt.Printf(
146                 "%s%s%s $ %s\n",
147                 m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "),
148         )
149         if len(m["ExitCode"]) > 0 {
150                 fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0])
151         }
152         finished, err := tai64n.Decode(m["Finished"][0])
153         if err != nil {
154                 return ErrLine(err)
155         }
156         durationSec, durationNsec, err := durationToInts(m["Duration"][0])
157         if err != nil {
158                 return ErrLine(err)
159         }
160         fmt.Printf(
161                 "%s%sStarted:\t%s\n%s%sFinished:\t%s\n%s%sDuration:\t%d.%ds\n\n",
162                 m["Started"][0], dp, started.Format(HumanTimeFmt),
163                 m["Started"][0], dp, finished.Format(HumanTimeFmt),
164                 m["Started"][0], dp, durationSec, durationNsec,
165         )
166         return nil
167 }
168
169 func showBuildLog(tgt *Tgt, buildLogRec map[string][]string, depth int) error {
170         var err error
171         if *flagBuildLogCommands || *flagBuildLogRecursive {
172                 buildLogRec, err = parseBuildLogRec(tgt)
173                 if err != nil {
174                         return err
175                 }
176         }
177         if *flagBuildLogCommands {
178                 if err = showBuildLogCmd(buildLogRec, depth); err != nil {
179                         return err
180                 }
181         }
182         fd, err := os.Open(path.Join(tgt.h, RedoDir, tgt.t+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(tgt.h, 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 }