]> Cypherpunks.ru repositories - goredo.git/blob - buildlog.go
isModified check must only look at ifchanges
[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+LogRecSuffix))
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         subs := make([]*BuildLogJob, 0, len(buildLogRec["Ifchange"]))
204         for _, dep := range buildLogRec["Ifchange"] {
205                 subDir, subTgt := cwdAndTgt(path.Join(dirNormalized, dep))
206                 if subDir == dirNormalized && subTgt == tgtNormalized {
207                         continue
208                 }
209                 rec, err := parseBuildLogRec(subDir, subTgt)
210                 if err != nil {
211                         if os.IsNotExist(err) {
212                                 continue
213                         }
214                         return err
215                 }
216                 if rec["Build"][0] != buildLogRec["Build"][0] {
217                         continue
218                 }
219                 started, err := tai64n.Decode(rec["Started"][0])
220                 if err != nil {
221                         return err
222                 }
223                 var exitCode int
224                 if len(rec["ExitCode"]) > 0 {
225                         exitCode, err = strconv.Atoi(rec["ExitCode"][0])
226                         if err != nil {
227                                 return err
228                         }
229                 }
230                 subs = append(subs, &BuildLogJob{
231                         dir:      dirNormalized,
232                         tgt:      dep,
233                         started:  started,
234                         exitCode: exitCode,
235                         rec:      rec,
236                 })
237         }
238         sort.Sort(ByStarted(subs))
239         scanner := bufio.NewScanner(fd)
240         var text string
241         var sep int
242         var t time.Time
243         var sub *BuildLogJob
244         if len(subs) > 0 {
245                 sub = subs[len(subs)-1]
246         }
247         dp := depthPrefix(depth)
248         for {
249                 if !scanner.Scan() {
250                         if err = scanner.Err(); err != nil {
251                                 return err
252                         }
253                         break
254                 }
255                 text = scanner.Text()
256                 if text[0] != '@' {
257                         return errors.New("unexpected non-TAI64Ned string")
258                 }
259                 sep = strings.IndexByte(text, byte(' '))
260                 if sep == -1 {
261                         sep = len(text)
262                 }
263                 t, err = tai64n.Decode(text[1:sep])
264                 if err != nil {
265                         return err
266                 }
267                 for sub != nil && t.After(sub.started) {
268                         if err = showBuildLogSub(sub, depth); err != nil {
269                                 return err
270                         }
271                         subs = subs[:len(subs)-1]
272                         if len(subs) > 0 {
273                                 sub = subs[len(subs)-1]
274                         } else {
275                                 sub = nil
276                         }
277                 }
278                 if depth == 0 {
279                         fmt.Println(text)
280                 } else {
281                         fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
282                 }
283         }
284         for i := len(subs); i > 0; i-- {
285                 sub = subs[i-1]
286                 if err = showBuildLogSub(sub, depth); err != nil {
287                         return err
288                 }
289         }
290         return nil
291 }