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