2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
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.
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.
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/>.
34 "go.cypherpunks.ru/recfile"
35 "go.cypherpunks.ru/tai64n/v2"
38 const HumanTimeFmt = "2006-01-02 15:04:05.000000000 Z07:00"
40 type BuildLogJob struct {
45 rec map[string][]string
48 type ByStarted []*BuildLogJob
50 func (a ByStarted) Len() int { return len(a) }
52 func (a ByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
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
60 return a[i].started.After(a[j].started)
64 flagBuildLogRecursive *bool
65 flagBuildLogCommands *bool
66 buildLogSeen map[string]struct{}
70 if CmdName() != CmdNameRedoLog {
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{})
78 func parseBuildLogRec(dir, tgt string) (map[string][]string, error) {
79 fd, err := os.Open(path.Join(dir, RedoDir, tgt+LogRecSuffix))
83 r := recfile.NewReader(bufio.NewReader(fd))
84 rec, err := r.NextMapWithSlice()
89 func depthPrefix(depth int) string {
93 return " " + colourize(CDebug, strings.Repeat("> ", depth))
96 func showBuildLogSub(sub *BuildLogJob, depth int) error {
97 abs, err := filepath.Abs(path.Join(sub.dir, sub.tgt))
101 if _, ok := buildLogSeen[abs]; ok {
104 buildLogSeen[abs] = struct{}{}
105 dp := depthPrefix(depth)
108 sub.rec["Started"][0], dp,
109 colourize(CRedo, "redo "+sub.tgt),
111 if err := showBuildLog(sub.dir, sub.tgt, sub.rec, depth+1); err != nil {
114 durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
118 if sub.exitCode > 0 {
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,
127 "%s%s%s (%d.%ds)\n\n",
128 sub.rec["Finished"][0], dp,
129 colourize(CRedo, "done "+sub.tgt),
130 durationSec, durationNsec,
136 func durationToInts(d string) (int64, int64, error) {
137 duration, err := strconv.ParseInt(d, 10, 64)
141 return duration / 1e9, (duration % 1e9) / 1000, nil
144 func showBuildLogCmd(m map[string][]string, depth int) error {
145 started, err := tai64n.Decode(m["Started"][0])
149 dp := depthPrefix(depth)
152 m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "),
154 if len(m["ExitCode"]) > 0 {
155 fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0])
157 finished, err := tai64n.Decode(m["Finished"][0])
161 durationSec, durationNsec, err := durationToInts(m["Duration"][0])
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,
174 func showBuildLog(dir, tgt string, buildLogRec map[string][]string, depth int) error {
176 dirNormalized, tgtNormalized := cwdAndTgt(path.Join(dir, tgt))
177 if *flagBuildLogCommands || *flagBuildLogRecursive {
178 buildLogRec, err = parseBuildLogRec(dirNormalized, tgtNormalized)
183 if *flagBuildLogCommands {
184 if err = showBuildLogCmd(buildLogRec, depth); err != nil {
188 fd, err := os.Open(path.Join(dirNormalized, RedoDir, tgtNormalized+LogSuffix))
192 if !*flagBuildLogRecursive {
193 w := bufio.NewWriter(os.Stdout)
194 _, err = io.Copy(w, bufio.NewReader(fd))
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 {
209 rec, err := parseBuildLogRec(subDir, subTgt)
211 if os.IsNotExist(err) {
216 if rec["Build"][0] != buildLogRec["Build"][0] {
219 started, err := tai64n.Decode(rec["Started"][0])
224 if len(rec["ExitCode"]) > 0 {
225 exitCode, err = strconv.Atoi(rec["ExitCode"][0])
230 subs = append(subs, &BuildLogJob{
238 sort.Sort(ByStarted(subs))
239 scanner := bufio.NewScanner(fd)
245 sub = subs[len(subs)-1]
247 dp := depthPrefix(depth)
250 if err = scanner.Err(); err != nil {
255 text = scanner.Text()
257 return errors.New("unexpected non-TAI64Ned string")
259 sep = strings.IndexByte(text, byte(' '))
263 t, err = tai64n.Decode(text[1:sep])
267 for sub != nil && t.After(sub.started) {
268 if err = showBuildLogSub(sub, depth); err != nil {
271 subs = subs[:len(subs)-1]
273 sub = subs[len(subs)-1]
281 fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
284 for i := len(subs); i > 0; i-- {
286 if err = showBuildLogSub(sub, depth); err != nil {