]> Cypherpunks.ru repositories - goredo.git/blob - buildlog.go
Do not use legacy os.IsNotExist
[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         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+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         abs := mustAbs(path.Join(sub.dir, sub.tgt))
98         if _, ok := buildLogSeen[abs]; ok {
99                 return nil
100         }
101         buildLogSeen[abs] = struct{}{}
102         dp := depthPrefix(depth)
103         fmt.Printf(
104                 "%s%s%s\n",
105                 sub.rec["Started"][0], dp,
106                 colourize(CRedo, "redo "+sub.tgt),
107         )
108         if err := showBuildLog(sub.dir, sub.tgt, sub.rec, depth+1); err != nil {
109                 return err
110         }
111         durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
112         if err != nil {
113                 return ErrLine(err)
114         }
115         if sub.exitCode > 0 {
116                 fmt.Printf(
117                         "%s%s%s (code: %d) (%d.%ds)\n\n",
118                         sub.rec["Finished"][0], dp,
119                         colourize(CErr, "err "+sub.tgt),
120                         sub.exitCode, durationSec, durationNsec,
121                 )
122         } else {
123                 fmt.Printf(
124                         "%s%s%s (%d.%ds)\n\n",
125                         sub.rec["Finished"][0], dp,
126                         colourize(CRedo, "done "+sub.tgt),
127                         durationSec, durationNsec,
128                 )
129         }
130         return nil
131 }
132
133 func durationToInts(d string) (int64, int64, error) {
134         duration, err := strconv.ParseInt(d, 10, 64)
135         if err != nil {
136                 return 0, 0, err
137         }
138         return duration / 1e9, (duration % 1e9) / 1000, nil
139 }
140
141 func showBuildLogCmd(m map[string][]string, depth int) error {
142         started, err := tai64n.Decode(m["Started"][0])
143         if err != nil {
144                 return ErrLine(err)
145         }
146         dp := depthPrefix(depth)
147         fmt.Printf(
148                 "%s%s%s $ %s\n",
149                 m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "),
150         )
151         if len(m["ExitCode"]) > 0 {
152                 fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0])
153         }
154         finished, err := tai64n.Decode(m["Finished"][0])
155         if err != nil {
156                 return ErrLine(err)
157         }
158         durationSec, durationNsec, err := durationToInts(m["Duration"][0])
159         if err != nil {
160                 return ErrLine(err)
161         }
162         fmt.Printf(
163                 "%s%sStarted:\t%s\n%s%sFinished:\t%s\n%s%sDuration:\t%d.%ds\n\n",
164                 m["Started"][0], dp, started.Format(HumanTimeFmt),
165                 m["Started"][0], dp, finished.Format(HumanTimeFmt),
166                 m["Started"][0], dp, durationSec, durationNsec,
167         )
168         return nil
169 }
170
171 func showBuildLog(dir, tgt string, buildLogRec map[string][]string, depth int) error {
172         var err error
173         dirNormalized, tgtNormalized := cwdAndTgt(path.Join(dir, tgt))
174         if *flagBuildLogCommands || *flagBuildLogRecursive {
175                 buildLogRec, err = parseBuildLogRec(dirNormalized, tgtNormalized)
176                 if err != nil {
177                         return err
178                 }
179         }
180         if *flagBuildLogCommands {
181                 if err = showBuildLogCmd(buildLogRec, depth); err != nil {
182                         return err
183                 }
184         }
185         fd, err := os.Open(path.Join(dirNormalized, RedoDir, tgtNormalized+LogSuffix))
186         if err != nil {
187                 return ErrLine(err)
188         }
189         if !*flagBuildLogRecursive {
190                 w := bufio.NewWriter(os.Stdout)
191                 _, err = io.Copy(w, bufio.NewReader(fd))
192                 fd.Close()
193                 if err != nil {
194                         w.Flush()
195                         return ErrLine(err)
196                 }
197                 return ErrLine(w.Flush())
198         }
199         defer fd.Close()
200         subs := make([]*BuildLogJob, 0, len(buildLogRec["Ifchange"]))
201         for _, dep := range buildLogRec["Ifchange"] {
202                 subDir, subTgt := cwdAndTgt(path.Join(dirNormalized, dep))
203                 if subDir == dirNormalized && subTgt == tgtNormalized {
204                         continue
205                 }
206                 rec, err := parseBuildLogRec(subDir, subTgt)
207                 if err != nil {
208                         if errors.Is(err, fs.ErrNotExist) {
209                                 continue
210                         }
211                         return err
212                 }
213                 if rec["Build"][0] != buildLogRec["Build"][0] {
214                         continue
215                 }
216                 started, err := tai64n.Decode(rec["Started"][0])
217                 if err != nil {
218                         return ErrLine(err)
219                 }
220                 var exitCode int
221                 if len(rec["ExitCode"]) > 0 {
222                         exitCode, err = strconv.Atoi(rec["ExitCode"][0])
223                         if err != nil {
224                                 return ErrLine(err)
225                         }
226                 }
227                 subs = append(subs, &BuildLogJob{
228                         dir:      dirNormalized,
229                         tgt:      dep,
230                         started:  started,
231                         exitCode: exitCode,
232                         rec:      rec,
233                 })
234         }
235         sort.Sort(ByStarted(subs))
236         scanner := bufio.NewScanner(fd)
237         var text string
238         var sep int
239         var t time.Time
240         var sub *BuildLogJob
241         if len(subs) > 0 {
242                 sub = subs[len(subs)-1]
243         }
244         dp := depthPrefix(depth)
245         for {
246                 if !scanner.Scan() {
247                         if err = scanner.Err(); err != nil {
248                                 return ErrLine(err)
249                         }
250                         break
251                 }
252                 text = scanner.Text()
253                 if text[0] != '@' {
254                         return ErrLine(errors.New("unexpected non-TAI64Ned string"))
255                 }
256                 sep = strings.IndexByte(text, byte(' '))
257                 if sep == -1 {
258                         sep = len(text)
259                 }
260                 t, err = tai64n.Decode(text[1:sep])
261                 if err != nil {
262                         return ErrLine(err)
263                 }
264                 for sub != nil && t.After(sub.started) {
265                         if err = showBuildLogSub(sub, depth); err != nil {
266                                 return err
267                         }
268                         subs = subs[:len(subs)-1]
269                         if len(subs) > 0 {
270                                 sub = subs[len(subs)-1]
271                         } else {
272                                 sub = nil
273                         }
274                 }
275                 if depth == 0 {
276                         fmt.Println(text)
277                 } else {
278                         fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
279                 }
280         }
281         for i := len(subs); i > 0; i-- {
282                 sub = subs[i-1]
283                 if err = showBuildLogSub(sub, depth); err != nil {
284                         return err
285                 }
286         }
287         return nil
288 }