]> Cypherpunks.ru repositories - goredo.git/blob - run.go
Do not overwrite unchanged target
[goredo.git] / run.go
1 /*
2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2022 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 // Targets runner
19
20 package main
21
22 import (
23         "bufio"
24         "errors"
25         "flag"
26         "fmt"
27         "io"
28         "log"
29         "os"
30         "os/exec"
31         "path"
32         "path/filepath"
33         "strconv"
34         "strings"
35         "sync"
36         "syscall"
37         "time"
38
39         "go.cypherpunks.ru/recfile"
40         "go.cypherpunks.ru/tai64n/v2"
41         "golang.org/x/sys/unix"
42 )
43
44 const (
45         EnvDepFd        = "REDO_DEP_FD"
46         EnvDirPrefix    = "REDO_DIRPREFIX"
47         EnvDepCwd       = "REDO_DEP_CWD"
48         EnvBuildUUID    = "REDO_BUILD_UUID"
49         EnvStderrPrefix = "REDO_STDERR_PREFIX"
50         EnvTrace        = "REDO_TRACE"
51         EnvStderrKeep   = "REDO_LOGS"
52         EnvStderrSilent = "REDO_SILENT"
53         EnvNoSync       = "REDO_NO_SYNC"
54         EnvStopIfMod    = "REDO_STOP_IF_MODIFIED"
55
56         RedoDir      = ".redo"
57         LockSuffix   = ".lock"
58         DepSuffix    = ".rec"
59         TmpPrefix    = ".redo."
60         LogSuffix    = ".log"
61         LogRecSuffix = ".log-rec"
62 )
63
64 var (
65         NoSync       = false
66         StderrKeep   = false
67         StderrSilent = false
68         StderrPrefix string
69         StopIfMod    = false
70         Jobs         sync.WaitGroup
71
72         flagTrace        *bool
73         flagTraceAll     *bool
74         flagStderrKeep   *bool
75         flagStderrSilent *bool
76
77         TracedAll bool
78
79         RunningProcs  = map[int]*os.Process{}
80         RunningProcsM sync.Mutex
81
82         Err1WasTouched = errors.New("$1 was explicitly touched")
83 )
84
85 func init() {
86         cmdName := CmdName()
87         if !(cmdName == CmdNameRedo || cmdName == CmdNameRedoIfchange) {
88                 return
89         }
90         flagTrace = flag.Bool("x", false, "trace (sh -x) current targets")
91         flagTraceAll = flag.Bool("xx", false,
92                 fmt.Sprintf("trace (sh -x) all targets (%s=1)", EnvTrace))
93         flagStderrKeep = flag.Bool("k", false,
94                 fmt.Sprintf("keep job's stderr (%s=1)", EnvStderrKeep))
95         flagStderrSilent = flag.Bool("s", false,
96                 fmt.Sprintf("silent, do not print job's stderr (%s=1)", EnvStderrSilent))
97 }
98
99 type RunError struct {
100         Tgt      string
101         DoFile   string
102         Started  *time.Time
103         Finished *time.Time
104         Err      error
105 }
106
107 func (e *RunError) Name() string {
108         var name string
109         if e.DoFile == "" {
110                 name = e.Tgt
111         } else {
112                 name = fmt.Sprintf("%s (%s)", e.Tgt, e.DoFile)
113         }
114         if e.Finished == nil {
115                 return name
116         }
117         return fmt.Sprintf("%s (%.3fs)", name, e.Finished.Sub(*e.Started).Seconds())
118 }
119
120 func (e RunError) Error() string {
121         return fmt.Sprintf("%s: %s", e.Name(), e.Err)
122 }
123
124 func mkdirs(pth string) error {
125         if _, err := os.Stat(pth); err == nil {
126                 return nil
127         }
128         return os.MkdirAll(pth, os.FileMode(0777))
129 }
130
131 func isModified(cwd, redoDir, tgt string) (bool, *Inode, string, error) {
132         fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
133         if err != nil {
134                 if os.IsNotExist(err) {
135                         return false, nil, "", nil
136                 }
137                 return false, nil, "", err
138         }
139         defer fdDep.Close()
140         r := recfile.NewReader(fdDep)
141         var modified bool
142         var ourInode *Inode
143         var hshPrev string
144         for {
145                 m, err := r.NextMap()
146                 if err != nil {
147                         if errors.Is(err, io.EOF) {
148                                 break
149                         }
150                         return false, nil, "", err
151                 }
152                 if m["Type"] != DepTypeIfchange || m["Target"] != tgt {
153                         continue
154                 }
155                 fd, err := os.Open(path.Join(cwd, tgt))
156                 if err != nil {
157                         if os.IsNotExist(err) {
158                                 return false, nil, "", nil
159                         }
160                         return false, nil, "", err
161                 }
162                 ourInode, err = inodeFromFile(fd)
163                 fd.Close()
164                 if err != nil {
165                         return false, nil, "", err
166                 }
167                 theirInode, err := inodeFromRec(m)
168                 if err != nil {
169                         return false, nil, "", err
170                 }
171                 hshPrev = m["Hash"]
172                 modified = !ourInode.Equals(theirInode)
173                 break
174         }
175         return modified, ourInode, hshPrev, nil
176 }
177
178 func syncDir(dir string) error {
179         fd, err := os.Open(dir)
180         if err != nil {
181                 return err
182         }
183         err = fd.Sync()
184         fd.Close()
185         return err
186 }
187
188 func runScript(tgtOrig string, errs chan error, traced bool) error {
189         cwd, tgt := cwdAndTgt(tgtOrig)
190         redoDir := path.Join(cwd, RedoDir)
191         if err := mkdirs(redoDir); err != nil {
192                 return TgtError{tgtOrig, err}
193         }
194
195         // Acquire lock
196         fdLock, err := os.OpenFile(
197                 path.Join(redoDir, tgt+LockSuffix),
198                 os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
199                 os.FileMode(0666),
200         )
201         if err != nil {
202                 return TgtError{tgtOrig, err}
203         }
204         flock := unix.Flock_t{
205                 Type:   unix.F_WRLCK,
206                 Whence: io.SeekStart,
207         }
208         lockRelease := func() {
209                 tracef(CLock, "LOCK_UN: %s", fdLock.Name())
210                 flock.Type = unix.F_UNLCK
211                 if err := unix.FcntlFlock(fdLock.Fd(), unix.F_SETLK, &flock); err != nil {
212                         log.Fatalln(err)
213                 }
214                 fdLock.Close()
215         }
216         tracef(CLock, "LOCK_NB: %s", fdLock.Name())
217
218         // Waiting for job completion, already taken by someone else
219         if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLK, &flock); err != nil {
220                 if uintptr(err.(syscall.Errno)) != uintptr(unix.EAGAIN) {
221                         fdLock.Close()
222                         return TgtError{tgtOrig, err}
223                 }
224                 Jobs.Add(1)
225                 if err = unix.FcntlFlock(fdLock.Fd(), unix.F_GETLK, &flock); err != nil {
226                         log.Fatalln(err)
227                 }
228                 tracef(CDebug, "waiting: %s (pid=%d)", tgtOrig, flock.Pid)
229                 if FdStatus != nil {
230                         if _, err = FdStatus.Write([]byte{StatusWait}); err != nil {
231                                 log.Fatalln(err)
232                         }
233                 }
234                 go func() {
235                         defer Jobs.Done()
236                         tracef(CLock, "LOCK_EX: %s", fdLock.Name())
237                         if err := unix.FcntlFlock(fdLock.Fd(), unix.F_SETLKW, &flock); err != nil {
238                                 log.Fatalln(err)
239                         }
240                         lockRelease()
241                         tracef(CDebug, "waiting done: %s", tgtOrig)
242                         if FdStatus != nil {
243                                 if _, err = FdStatus.Write([]byte{StatusWaited}); err != nil {
244                                         log.Fatalln(err)
245                                 }
246                         }
247                         var depInfo *DepInfo
248                         fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
249                         if err != nil {
250                                 if os.IsNotExist(err) {
251                                         err = errors.New("was not built: no .rec")
252                                 }
253                                 goto Finish
254                         }
255                         defer fdDep.Close()
256                         depInfo, err = depRead(fdDep)
257                         if err != nil {
258                                 goto Finish
259                         }
260                         if depInfo.build != BuildUUID {
261                                 err = errors.New("was not built: build differs")
262                         }
263                 Finish:
264                         if err != nil {
265                                 err = TgtError{tgtOrig, err}
266                         }
267                         errs <- err
268                 }()
269                 return nil
270         }
271
272         // Check if target is not modified externally
273         modified, inodePrev, hshPrev, err := isModified(cwd, redoDir, tgt)
274         if err != nil {
275                 lockRelease()
276                 return TgtError{tgtOrig, err}
277         }
278         if modified {
279                 lockRelease()
280                 if StopIfMod {
281                         return fmt.Errorf("%s externally modified", tgtOrig)
282                 }
283                 tracef(CWarn, "%s externally modified: not redoing", tgtOrig)
284                 go func() {
285                         errs <- nil
286                 }()
287                 return nil
288         }
289
290         // Start preparing .rec
291         fdDep, err := tempfile(redoDir, tgt+DepSuffix)
292         if err != nil {
293                 lockRelease()
294                 return TgtError{tgtOrig, err}
295         }
296         fdDepPath := fdDep.Name()
297         cleanup := func() {
298                 lockRelease()
299                 fdDep.Close()
300                 os.Remove(fdDep.Name())
301         }
302         if _, err = recfile.NewWriter(fdDep).WriteFields(
303                 recfile.Field{Name: "Build", Value: BuildUUID},
304         ); err != nil {
305                 cleanup()
306                 return TgtError{tgtOrig, err}
307         }
308
309         // Find .do
310         doFile, upLevels, err := findDo(fdDep, cwd, tgt)
311         if err != nil {
312                 cleanup()
313                 return TgtError{tgtOrig, err}
314         }
315         if doFile == "" {
316                 cleanup()
317                 return TgtError{tgtOrig, errors.New("no .do found")}
318         }
319
320         // Determine basename and DIRPREFIX
321         doFileRelPath := doFile
322         ents := strings.Split(cwd, "/")
323         ents = ents[len(ents)-upLevels:]
324         dirPrefix := path.Join(ents...)
325         cwdOrig := cwd
326         for i := 0; i < upLevels; i++ {
327                 cwd = path.Join(cwd, "..")
328                 doFileRelPath = path.Join("..", doFileRelPath)
329         }
330         cwd = path.Clean(cwd)
331         doFilePath := path.Join(cwd, doFile)
332         basename := tgt
333         runErr := RunError{Tgt: tgtOrig}
334         if strings.HasPrefix(doFile, "default.") {
335                 basename = tgt[:len(tgt)-(len(doFile)-len("default.")-len(".do"))-1]
336                 runErr.DoFile = doFileRelPath
337         }
338
339         if err = depWrite(fdDep, cwdOrig, doFileRelPath, ""); err != nil {
340                 cleanup()
341                 return TgtError{tgtOrig, err}
342         }
343         fdDep.Close()
344         tracef(CWait, "%s", runErr.Name())
345
346         // Prepare command line
347         var cmdName string
348         var args []string
349         if err = unix.Access(doFilePath, unix.X_OK); err == nil {
350                 cmdName = doFilePath
351                 args = make([]string, 0, 3)
352         } else {
353                 cmdName = "/bin/sh"
354                 if traced || TracedAll {
355                         args = append(args, "-ex")
356                 } else {
357                         args = append(args, "-e")
358                 }
359                 args = append(args, doFile)
360         }
361
362         // Temporary file for stdout
363         fdStdout, err := tempfile(cwdOrig, tgt)
364         if err != nil {
365                 cleanup()
366                 return TgtError{tgtOrig, err}
367         }
368         stdoutPath := fdStdout.Name()
369         fdStdout.Close()
370         tmpPath := stdoutPath + ".3" // and for $3
371         tmpPathRel, err := filepath.Rel(cwd, tmpPath)
372         if err != nil {
373                 panic(err)
374         }
375         args = append(
376                 args,
377                 path.Join(dirPrefix, tgt),
378                 path.Join(dirPrefix, basename),
379                 tmpPathRel,
380         )
381
382         cmd := exec.Command(cmdName, args...)
383         cmd.Dir = cwd
384         // cmd.Stdin reads from /dev/null by default
385         cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%d", EnvLevel, Level+1))
386         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvDirPrefix, dirPrefix))
387         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvBuildUUID, BuildUUID))
388
389         childStderrPrefix := tempsuffix()
390         cmd.Env = append(cmd.Env, fmt.Sprintf(
391                 "%s=%s", EnvStderrPrefix, childStderrPrefix,
392         ))
393
394         fdNum := 0
395         cmd.ExtraFiles = append(cmd.ExtraFiles, FdOODTgts)
396         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvOODTgtsFd, 3+fdNum))
397         fdNum++
398         cmd.ExtraFiles = append(cmd.ExtraFiles, FdOODTgtsLock)
399         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvOODTgtsLockFd, 3+fdNum))
400         fdNum++
401
402         if FdStatus == nil {
403                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvStatusFd))
404         } else {
405                 cmd.ExtraFiles = append(cmd.ExtraFiles, FdStatus)
406                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvStatusFd, 3+fdNum))
407                 fdNum++
408         }
409
410         // Preparing stderr
411         var fdStderr *os.File
412         if StderrKeep {
413                 fdStderr, err = os.OpenFile(
414                         path.Join(redoDir, tgt+LogSuffix),
415                         os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
416                         os.FileMode(0666),
417                 )
418                 if err != nil {
419                         cleanup()
420                         return TgtError{tgtOrig, err}
421                 }
422         }
423         shCtx := fmt.Sprintf(
424                 "sh: %s: %s %s cwd:%s dirprefix:%s",
425                 tgtOrig, cmdName, args, cwd, dirPrefix,
426         )
427         tracef(CDebug, "%s", shCtx)
428
429         Jobs.Add(1)
430         go func() {
431                 jsToken := jsAcquire(shCtx)
432                 if JSR == nil {
433                         // infinite jobs
434                         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvJobs))
435                 } else {
436                         cmd.ExtraFiles = append(cmd.ExtraFiles, JSR)
437                         cmd.ExtraFiles = append(cmd.ExtraFiles, JSW)
438                         makeFlags := fmt.Sprintf(
439                                 "%s %s%d,%d", MakeFlags, MakeJSArg, 3+fdNum+0, 3+fdNum+1,
440                         )
441                         makeFlags = strings.Trim(makeFlags, " ")
442                         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", MakeFlagsName, makeFlags))
443                         fdNum += 2
444                         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvJSToken, jsToken))
445                 }
446
447                 if FdStatus != nil {
448                         if _, err = FdStatus.Write([]byte{StatusRun}); err != nil {
449                                 log.Fatalln(err)
450                         }
451                 }
452
453                 var finished time.Time
454                 var exitErr *exec.ExitError
455                 started := time.Now()
456                 runErr.Started = &started
457                 fdStdout, err = os.OpenFile(stdoutPath, os.O_RDWR, os.FileMode(0666))
458                 if err != nil {
459                         runErr.Err = err
460                         errs <- runErr
461                         return
462                 }
463                 cmd.Stdout = fdStdout
464                 fdDep, err = os.OpenFile(fdDepPath, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
465                 if err != nil {
466                         runErr.Err = err
467                         errs <- runErr
468                         return
469                 }
470                 cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep)
471                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvDepFd, 3+fdNum))
472                 fdNum++
473                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvDepCwd, cwd))
474
475                 defer func() {
476                         jsRelease(shCtx, jsToken)
477                         fdDep.Close()
478                         fdStdout.Close()
479                         if fdStderr != nil {
480                                 fdStderr.Close()
481                                 logRecPath := path.Join(redoDir, tgt+LogRecSuffix)
482                                 if fdStderr, err = os.OpenFile(
483                                         logRecPath,
484                                         os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
485                                         os.FileMode(0666),
486                                 ); err == nil {
487                                         fields := []recfile.Field{
488                                                 {Name: "Build", Value: BuildUUID},
489                                                 {Name: "PPID", Value: strconv.Itoa(os.Getpid())},
490                                                 {Name: "Cwd", Value: cwd},
491                                         }
492                                         if cmd.Process != nil {
493                                                 fields = append(fields, recfile.Field{
494                                                         Name: "PID", Value: strconv.Itoa(cmd.Process.Pid),
495                                                 })
496                                         }
497                                         ts := new(tai64n.TAI64N)
498                                         ts.FromTime(started)
499                                         fields = append(fields,
500                                                 recfile.Field{Name: "Started", Value: tai64n.Encode(ts[:])},
501                                         )
502                                         ts.FromTime(finished)
503                                         fields = append(fields,
504                                                 recfile.Field{Name: "Finished", Value: tai64n.Encode(ts[:])})
505                                         fields = append(fields, recfile.Field{
506                                                 Name:  "Duration",
507                                                 Value: strconv.FormatInt(finished.Sub(started).Nanoseconds(), 10),
508                                         })
509                                         fields = append(fields, recfile.Field{Name: "Cmd", Value: cmdName})
510                                         for _, arg := range args {
511                                                 fields = append(fields, recfile.Field{Name: "Arg", Value: arg})
512                                         }
513                                         for _, env := range cmd.Env {
514                                                 fields = append(fields, recfile.Field{Name: "Env", Value: env})
515                                         }
516                                         if exitErr != nil {
517                                                 fields = append(fields, recfile.Field{
518                                                         Name:  "ExitCode",
519                                                         Value: strconv.Itoa(exitErr.ProcessState.ExitCode()),
520                                                 })
521                                         }
522                                         w := bufio.NewWriter(fdStderr)
523
524                                         var depInfo *DepInfo
525                                         fdDep, err := os.Open(fdDepPath)
526                                         if err != nil {
527                                                 goto Err
528                                         }
529                                         depInfo, err = depRead(fdDep)
530                                         fdDep.Close()
531                                         if err != nil {
532                                                 goto Err
533                                         }
534                                         for _, dep := range depInfo.ifchanges {
535                                                 fields = append(fields, recfile.Field{
536                                                         Name:  "Ifchange",
537                                                         Value: dep["Target"],
538                                                 })
539                                         }
540                                         _, err = recfile.NewWriter(w).WriteFields(fields...)
541                                         if err != nil {
542                                                 goto Err
543                                         }
544                                         err = w.Flush()
545                                 Err:
546                                         if err != nil {
547                                                 log.Println(err)
548                                                 os.Remove(logRecPath)
549                                         }
550                                         fdStderr.Close()
551                                 } else {
552                                         log.Println("can not open", logRecPath, ":", err)
553                                 }
554                         }
555                         lockRelease()
556                         os.Remove(fdDep.Name())
557                         os.Remove(fdStdout.Name())
558                         os.Remove(tmpPath)
559                         os.Remove(fdLock.Name())
560                         if FdStatus != nil {
561                                 if _, err = FdStatus.Write([]byte{StatusDone}); err != nil {
562                                         log.Fatalln(err)
563                                 }
564                         }
565                         Jobs.Done()
566                 }()
567                 stderr, err := cmd.StderrPipe()
568                 if err != nil {
569                         runErr.Err = err
570                         errs <- runErr
571                         return
572                 }
573                 started = time.Now()
574                 err = cmd.Start()
575                 if err != nil {
576                         runErr.Err = err
577                         errs <- runErr
578                         return
579                 }
580                 RunningProcsM.Lock()
581                 RunningProcs[cmd.Process.Pid] = cmd.Process
582                 RunningProcsM.Unlock()
583                 pid := fmt.Sprintf("[%d]", cmd.Process.Pid)
584                 tracef(CDebug, "%s runs %s", tgtOrig, pid)
585
586                 stderrTerm := make(chan struct{})
587                 go func() {
588                         scanner := bufio.NewScanner(stderr)
589                         var line string
590                         ts := new(tai64n.TAI64N)
591                         for scanner.Scan() {
592                                 line = scanner.Text()
593                                 if strings.HasPrefix(line, childStderrPrefix) {
594                                         line = line[len(childStderrPrefix):]
595                                         os.Stderr.WriteString(StderrPrefix + line + "\n")
596                                         continue
597                                 }
598                                 if fdStderr != nil {
599                                         ts.FromTime(time.Now())
600                                         LogMutex.Lock()
601                                         fmt.Fprintln(fdStderr, tai64n.Encode(ts[:]), line)
602                                         LogMutex.Unlock()
603                                 }
604                                 if StderrSilent {
605                                         continue
606                                 }
607                                 if MyPid == 0 {
608                                         tracef(CNone, "%s", line)
609                                 } else {
610                                         tracef(CNone, "%s %s", pid, line)
611                                 }
612                         }
613                         close(stderrTerm)
614                 }()
615
616                 // Wait for job completion
617                 <-stderrTerm
618                 err = cmd.Wait()
619                 RunningProcsM.Lock()
620                 delete(RunningProcs, cmd.Process.Pid)
621                 RunningProcsM.Unlock()
622                 finished = time.Now()
623                 runErr.Finished = &finished
624                 if err != nil {
625                         exitErr = err.(*exec.ExitError)
626                         runErr.Err = err
627                         errs <- runErr
628                         return
629                 }
630
631                 // Was $1 touched?
632                 if fd, err := os.Open(path.Join(cwdOrig, tgt)); err == nil {
633                         if inodePrev == nil {
634                                 fd.Close()
635                                 runErr.Err = Err1WasTouched
636                                 errs <- runErr
637                                 return
638                         }
639                         inode, err := inodeFromFile(fd)
640                         fd.Close()
641                         if err != nil {
642                                 runErr.Err = err
643                                 errs <- runErr
644                                 return
645                         }
646                         if !inode.Equals(inodePrev) {
647                                 runErr.Err = Err1WasTouched
648                                 errs <- runErr
649                                 return
650                         }
651                 }
652
653                 if inodePrev != nil {
654                         if fd, err := os.Open(path.Join(cwdOrig, tgt)); err == nil {
655                                 inode, err := inodeFromFile(fd)
656                                 fd.Close()
657                                 if err == nil && !inode.Equals(inodePrev) {
658                                         runErr.Err = Err1WasTouched
659                                         errs <- runErr
660                                         return
661                                 }
662                         }
663                 }
664
665                 // Does it produce both stdout and tmp?
666                 fiStdout, err := os.Stat(fdStdout.Name())
667                 if err != nil {
668                         runErr.Err = err
669                         errs <- runErr
670                         return
671                 }
672                 tmpExists := false
673                 _, err = os.Stat(tmpPath)
674                 if err == nil {
675                         if fiStdout.Size() > 0 {
676                                 runErr.Err = errors.New("created both tmp and stdout")
677                                 errs <- runErr
678                                 return
679                         }
680                         tmpExists = true
681                 } else if !os.IsNotExist(err) {
682                         runErr.Err = err
683                         errs <- runErr
684                         return
685                 }
686
687                 // Determine what file we must process at last
688                 var fd *os.File
689                 if tmpExists {
690                         fd, err = os.Open(tmpPath)
691                         if err != nil {
692                                 goto Finish
693                         }
694                         defer fd.Close()
695                 } else if fiStdout.Size() > 0 {
696                         fd = fdStdout
697                 }
698
699                 // Do we need to ifcreate it, or ifchange with renaming?
700                 if fd == nil {
701                         os.Remove(path.Join(cwdOrig, tgt))
702                         err = ifcreate(fdDep, tgt)
703                         if err != nil {
704                                 goto Finish
705                         }
706                 } else {
707                         var hsh string
708                         if hshPrev != "" {
709                                 _, err = fd.Seek(0, io.SeekStart)
710                                 if err != nil {
711                                         goto Finish
712                                 }
713                                 hsh, err = fileHash(fd)
714                                 if err != nil {
715                                         goto Finish
716                                 }
717                                 if hsh == hshPrev {
718                                         tracef(CDebug, "%s has same hash, not renaming", tgtOrig)
719                                         err = os.Remove(fd.Name())
720                                         if err != nil {
721                                                 goto Finish
722                                         }
723                                         err = os.Chtimes(path.Join(cwdOrig, tgt), finished, finished)
724                                         if err != nil {
725                                                 goto Finish
726                                         }
727                                         if !NoSync {
728                                                 err = syncDir(cwdOrig)
729                                                 if err != nil {
730                                                         goto Finish
731                                                 }
732                                         }
733                                         err = depWrite(fdDep, cwdOrig, tgt, hshPrev)
734                                         if err != nil {
735                                                 goto Finish
736                                         }
737                                         goto RecCommit
738                                 }
739                         }
740                         if !NoSync {
741                                 err = fd.Sync()
742                                 if err != nil {
743                                         goto Finish
744                                 }
745                         }
746                         err = os.Rename(fd.Name(), path.Join(cwdOrig, tgt))
747                         if err != nil {
748                                 goto Finish
749                         }
750                         if !NoSync {
751                                 err = syncDir(cwdOrig)
752                                 if err != nil {
753                                         goto Finish
754                                 }
755                         }
756                         err = depWrite(fdDep, cwdOrig, tgt, hsh)
757                         if err != nil {
758                                 goto Finish
759                         }
760                 }
761
762         RecCommit:
763                 // Commit .rec
764                 if !NoSync {
765                         err = fdDep.Sync()
766                         if err != nil {
767                                 goto Finish
768                         }
769                 }
770                 fdDepPath = path.Join(redoDir, tgt+DepSuffix)
771                 err = os.Rename(fdDep.Name(), fdDepPath)
772                 if err != nil {
773                         goto Finish
774                 }
775                 if !NoSync {
776                         err = syncDir(redoDir)
777                         if err != nil {
778                                 goto Finish
779                         }
780                 }
781
782                 // Post-commit .rec sanitizing
783                 fdDep.Close()
784                 if fdDepR, err := os.Open(fdDepPath); err == nil {
785                         depInfo, err := depRead(fdDepR)
786                         fdDepR.Close()
787                         if err != nil {
788                                 goto Finish
789                         }
790                         ifchangeSeen := make(map[string]struct{}, len(depInfo.ifchanges))
791                         for _, dep := range depInfo.ifchanges {
792                                 ifchangeSeen[dep["Target"]] = struct{}{}
793                         }
794                         for _, dep := range depInfo.ifcreates {
795                                 if _, exists := ifchangeSeen[dep]; exists {
796                                         tracef(CWarn, "simultaneous ifcreate and ifchange records: %s", tgt)
797                                 }
798                         }
799                 }
800
801         Finish:
802                 runErr.Err = err
803                 errs <- runErr
804         }()
805         return nil
806 }
807
808 func isOkRun(err error) bool {
809         if err == nil {
810                 return true
811         }
812         var runErr RunError
813         if errors.As(err, &runErr) && runErr.Err == nil {
814                 tracef(CRedo, "%s", runErr.Name())
815                 return true
816         }
817         tracef(CErr, "%s", err)
818         return false
819 }