// goredo -- djb's redo implementation on pure Go
// Copyright (C) 2020-2026 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

// Targets runner

package main

import (
	"bufio"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"log"
	mrand "math/rand"
	"os"
	"os/exec"
	"path"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/google/uuid"
	"go.stargrave.org/recfile/v4"
	"go.stargrave.org/tai64n/v5"
	"golang.org/x/sys/unix"
)

const (
	EnvDepFd        = "REDO_DEP_FD"
	EnvDirPrefix    = "REDO_DIRPREFIX"
	EnvDepCwd       = "REDO_DEP_CWD"
	EnvBuildUUID    = "REDO_BUILD_UUID"
	EnvStderrPrefix = "REDO_STDERR_PREFIX"
	EnvTrace        = "REDO_TRACE"
	EnvStderrKeep   = "REDO_LOGS"
	EnvStderrSilent = "REDO_SILENT"
	EnvNoSync       = "REDO_NO_SYNC"
	EnvStopIfMod    = "REDO_STOP_IF_MODIFIED"

	RedoDir      = ".redo"
	LockSuffix   = ".lock"
	DepSuffix    = ".dep"
	TmpPrefix    = ".redo."
	LogSuffix    = ".log"
	LogRecSuffix = ".log-rec"
)

var (
	NoSync       = false
	StderrKeep   = false
	StderrSilent = false
	StderrPrefix string
	StopIfMod    = false
	Jobs         sync.WaitGroup
	TmpsToClean  = make(map[string]struct{})
	TmpsToCleanM sync.Mutex

	flagTrace          *bool
	flagTraceAll       *bool
	flagStderrKeep     *bool
	flagStderrSilent   *bool
	flagForcedIfchange *bool

	TracedAll bool

	RunningProcs  = map[int]*os.Process{}
	RunningProcsM sync.Mutex

	Err1WasTouched = errors.New("$1 was explicitly touched")
)

func init() {
	cmdName := CmdName()
	if !(cmdName == CmdNameRedo || cmdName == CmdNameRedoIfchange) {
		return
	}
	flagTrace = flag.Bool("x", false, "trace (sh -x) current targets")
	flagTraceAll = flag.Bool("xx", false,
		fmt.Sprintf("trace (sh -x) all targets (%s=1)", EnvTrace))
	flagStderrKeep = flag.Bool("k", false,
		fmt.Sprintf("keep job's stderr (%s=1)", EnvStderrKeep))
	flagStderrSilent = flag.Bool("s", false,
		fmt.Sprintf("silent, do not print job's stderr (%s=1)", EnvStderrSilent))
	flagForcedIfchange = flag.Bool("f", false, "forced redo-ifchange")
}

type RunError struct {
	Err      error
	Tgt      *Tgt
	Started  *time.Time
	Finished *time.Time
	DoFile   string
}

func (e *RunError) Name() string {
	var name string
	if e.DoFile == "" {
		name = e.Tgt.rel
	} else {
		name = fmt.Sprintf("%s (%s)", e.Tgt, e.DoFile)
	}
	if e.Finished == nil {
		return name
	}
	return fmt.Sprintf("%s (%.3fs)", name, e.Finished.Sub(*e.Started).Seconds())
}

func (e RunError) Error() string {
	return fmt.Sprintf("%s: %s", e.Name(), e.Err)
}

func mkdirs(pth string) error {
	if FileExists(pth) {
		return nil
	}
	return os.MkdirAll(pth, os.FileMode(0o777))
}

func isModified(dep *Dep, tgt *Tgt) (
	modified bool, ourInode *Inode, hshPrev Hash, err error,
) {
	if dep == nil {
		return
	}
	for _, ifchange := range dep.ifchanges {
		if ifchange.tgt.a != tgt.a {
			continue
		}
		ourInode, err = inodeFromFileByPath(tgt.a)
		if err != nil {
			if os.IsNotExist(err) {
				err = nil
				return
			}
			err = ErrLine(err)
			return
		}
		hshPrev = ifchange.Hash()
		modified = !ourInode.Equals(ifchange.Inode())
		break
	}
	return
}

func syncDir(dir string) error {
	fd, err := os.Open(dir)
	if err != nil {
		return err
	}
	err = fd.Sync()
	fd.Close()
	return err
}

func runScript(tgt *Tgt, errs chan error, forced, traced bool) error {
	tgtH, tgtT := pathSplit(tgt.a)
	redoDir := path.Join(tgtH, RedoDir)
	if err := mkdirs(redoDir); err != nil {
		return TgtError{tgt, ErrLine(err)}
	}

	shCtx := fmt.Sprintf("sh: %s: cwd:%s", tgt, tgtH)
	jsToken := jsAcquire(shCtx)
	jsNeedsRelease := true
	defer func() {
		if jsNeedsRelease {
			jsRelease(shCtx, jsToken)
		}
	}()

	// Acquire lock
	fdLock, err := os.OpenFile(
		path.Join(redoDir, tgtT+LockSuffix),
		os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
		os.FileMode(0o666),
	)
	if err != nil {
		return TgtError{tgt, ErrLine(err)}
	}
	flock := unix.Flock_t{
		Type:   unix.F_WRLCK,
		Whence: io.SeekStart,
	}
	lockRelease := func() {
		tracef(CLock, "LOCK_UN: %s", fdLock.Name())
		flock.Type = unix.F_UNLCK
		if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLK, &flock); err != nil {
			log.Fatalln(err, fdLock.Name())
		}
		fdLock.Close()
	}
	tracef(CLock, "LOCK_NB: %s", fdLock.Name())

	// Waiting for job completion, already taken by someone else
	if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLK, &flock); err != nil {
		if uintptr(err.(syscall.Errno)) != uintptr(unix.EAGAIN) {
			fdLock.Close()
			return TgtError{tgt, ErrLine(err)}
		}
		Jobs.Add(1)
		if err = unix.FcntlFlock(fdLock.Fd(), unix.F_GETLK, &flock); err != nil {
			log.Fatalln(err, fdLock.Name())
		}
		tracef(CDebug, "waiting: %s (pid=%d)", tgt, flock.Pid)
		if FdStatus != nil {
			if _, err = FdStatus.Write([]byte{StatusWait}); err != nil {
				log.Fatal(err)
			}
		}
		go func() {
			defer Jobs.Done()
			tracef(CLock, "LOCK_EX: %s", fdLock.Name())
		LockAgain:
			if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLKW, &flock); err != nil {
				if errors.Is(err, unix.EDEADLK) || errors.Is(err, unix.EINTR) {
					time.Sleep(time.Duration(1+mrand.Intn(20)) * time.Millisecond)
					goto LockAgain
				}
				log.Fatalln(err, fdLock.Name())
			}
			lockRelease()
			tracef(CDebug, "waiting done: %s", tgt)
			if FdStatus != nil {
				if _, err = FdStatus.Write([]byte{StatusWaited}); err != nil {
					log.Fatal(err)
				}
			}
			var build uuid.UUID
			build, err = depBuildRead(tgt.dep)
			if err == nil {
				if build != BuildUUID {
					err = errors.New("was not built: build differs")
				}
			} else {
				if errors.Is(err, fs.ErrNotExist) {
					err = errors.New("was not built: no .dep")
				}
			}
			if err != nil {
				err = TgtError{tgt, err}
			}
			errs <- err
		}()
		return nil
	}

	// It scans the whole .dep file while searching for the single target,
	// but that one is always located at the very end
	dep, err := depRead(tgt)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			err = nil
		} else {
			lockRelease()
			return TgtError{tgt, ErrLine(err)}
		}
	}

	// Check if it was already built in parallel
	if !forced && dep != nil && dep.build == BuildUUID {
		lockRelease()
		errs <- nil
		return nil
	}

	// Check if target is not modified externally
	modified, inodePrev, hshPrev, err := isModified(dep, tgt)
	if err != nil {
		lockRelease()
		return TgtError{tgt, ErrLine(err)}
	}
	if modified {
		lockRelease()
		if StopIfMod {
			return fmt.Errorf("%s externally modified", tgt)
		}
		Jobs.Add(1)
		tracef(CWarn, "%s: externally modified: not redoing", tgt)
		go func() {
			errs <- nil
			Jobs.Done()
		}()
		return nil
	}
	if inodePrev == nil && FileExists(tgt.a) {
		lockRelease()
		if StopIfMod {
			return fmt.Errorf("%s already existing", tgt)
		}
		Jobs.Add(1)
		tracef(CWarn, "%s: already existing: not redoing", tgt)
		go func() {
			errs <- nil
			Jobs.Done()
		}()
		return nil
	}
	dep = nil

	// Start preparing .dep
	fdDep, err := tempfile(redoDir, tgtT+DepSuffix)
	if err != nil {
		lockRelease()
		return TgtError{tgt, ErrLine(err)}
	}
	{
		TmpsToCleanM.Lock()
		TmpsToClean[fdDep.Name()] = struct{}{}
		TmpsToCleanM.Unlock()
	}
	fdDepOpened := true
	fdDepExists := true
	fdDepW := bufio.NewWriter(fdDep)
	cleanup := func() {
		lockRelease()
		if fdDepOpened {
			fdDep.Close()
		}
		if fdDepExists {
			{
				TmpsToCleanM.Lock()
				delete(TmpsToClean, fdDep.Name())
				TmpsToCleanM.Unlock()
			}
			os.Remove(fdDep.Name())
		}
	}
	if err = depBuildWrite(fdDepW, BuildUUID); err != nil {
		cleanup()
		return TgtError{tgt, ErrLine(err)}
	}

	var cwd string
	var dirPrefix string
	var doFile *Tgt
	var doFileT string
	basename := tgtT
	runErr := RunError{Tgt: tgt}
	// Determine basename and DIRPREFIX
	{
		var doFileRelPath string
		var upLevels int
		doFileRelPath, upLevels, err = findDo(fdDepW, fdDep.Name(), tgtH, tgtT)
		if err != nil {
			cleanup()
			return TgtError{tgt, ErrLine(err)}
		}
		if doFileRelPath == "" {
			cleanup()
			return TgtError{tgt, errors.New("no .do found")}
		}
		ents := strings.Split(tgtH, "/")
		ents = ents[len(ents)-upLevels:]
		dirPrefix = path.Join(ents...)
		ups := make([]string, 0, upLevels+2)
		ups = append(ups, tgtH)
		for i := 0; i < upLevels; i++ {
			ups = append(ups, "..")
		}
		ups = append(ups, doFileRelPath)
		cwd = path.Clean(path.Join(ups[:len(ups)-1]...))
		doFile = NewTgt(path.Join(ups...))
		doFileT = path.Base(doFile.a)
		if strings.HasPrefix(doFileT, "default.") {
			basename = basename[:len(basename)-(len(doFileT)-len("default.")-len(".do"))-1]
			runErr.DoFile = doFile.rel
		}
	}

	if err = depWrite(fdDepW, fdDep.Name(), tgtH, doFile, HashNull); err != nil {
		cleanup()
		return TgtError{tgt, ErrLine(err)}
	}
	if err = fdDepW.Flush(); err != nil {
		cleanup()
		return TgtError{tgt, ErrLine(err)}
	}
	fdDep.Close()
	fdDepOpened = false
	tracef(CWait, "%s", runErr.Name())

	// Prepare command line
	var cmdName string
	var args []string
	if err = unix.Access(doFile.a, unix.X_OK); err == nil {
		cmdName = doFile.a
		args = make([]string, 0, 3)
	} else {
		cmdName = "/bin/sh"
		if traced || TracedAll {
			args = append(args, "-ex")
		} else {
			args = append(args, "-e")
		}
		args = append(args, doFileT)
	}

	// Temporary file for stdout
	fdStdout, err := tempfile(tgtH, tgtT)
	if err != nil {
		cleanup()
		return TgtError{tgt, ErrLine(err)}
	}
	{
		TmpsToCleanM.Lock()
		TmpsToClean[fdStdout.Name()] = struct{}{}
		TmpsToCleanM.Unlock()
	}
	fdStdout.Close()
	tmpPath := fdStdout.Name() + ".3" // and for $3
	tmpPathRel := mustRel(cwd, tmpPath)
	args = append(
		args,
		path.Join(dirPrefix, tgtT),
		path.Join(dirPrefix, basename),
		tmpPathRel,
	)
	shCtx = fmt.Sprintf(
		"sh: %s: %s %s cwd:%s dirprefix:%s",
		tgt, cmdName, args, cwd, dirPrefix,
	)

	cmd := exec.Command(cmdName, args...)
	cmd.Dir = cwd
	// cmd.Stdin reads from /dev/null by default
	cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%d", EnvLevel, Level+1))
	cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvDirPrefix, dirPrefix))
	cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvBuildUUID, BuildUUID))

	childStderrPrefixRaw := make([]byte, 8)
	if _, err = io.ReadFull(rand.Reader, childStderrPrefixRaw); err != nil {
		panic(err)
	}
	childStderrPrefix := hex.EncodeToString(childStderrPrefixRaw)
	cmd.Env = append(cmd.Env, fmt.Sprintf(
		"%s=%s", EnvStderrPrefix, childStderrPrefix,
	))

	fdNum := 0
	cmd.ExtraFiles = append(cmd.ExtraFiles, FdOODTgts)
	cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvOODTgtsFd, 3+fdNum))
	fdNum++
	cmd.ExtraFiles = append(cmd.ExtraFiles, FdOODTgtsLock)
	cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvOODTgtsLockFd, 3+fdNum))
	fdNum++

	if FdStatus == nil {
		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvStatusFd))
	} else {
		cmd.ExtraFiles = append(cmd.ExtraFiles, FdStatus)
		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvStatusFd, 3+fdNum))
		fdNum++
	}

	// Preparing stderr
	var fdStderr *os.File
	if StderrKeep {
		fdStderr, err = os.OpenFile(
			path.Join(redoDir, tgtT+LogSuffix),
			os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
			os.FileMode(0o666),
		)
		if err != nil {
			cleanup()
			return TgtError{tgt, ErrLine(err)}
		}
	}
	tracef(CDebug, "%s", shCtx)

	jsNeedsRelease = false
	Jobs.Add(1)
	go func() {
		if JSR == nil {
			// infinite jobs
			cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvJobs))
		} else {
			cmd.ExtraFiles = append(cmd.ExtraFiles, JSR)
			cmd.ExtraFiles = append(cmd.ExtraFiles, JSW)
			makeFlags := fmt.Sprintf(
				"%s %s%d,%d", MakeFlags, MakeJSArg, 3+fdNum+0, 3+fdNum+1,
			)
			makeFlags = strings.Trim(makeFlags, " ")
			cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", MakeFlagsName, makeFlags))
			fdNum += 2
			cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvJSToken, jsToken))
		}

		if FdStatus != nil {
			if _, err = FdStatus.Write([]byte{StatusRun}); err != nil {
				log.Fatal(err)
			}
		}

		defer jsRelease(shCtx, jsToken)

		var finished time.Time
		var exitErr *exec.ExitError
		started := time.Now()
		runErr.Started = &started
		fdStdout, err = os.OpenFile(fdStdout.Name(), os.O_RDWR, os.FileMode(0o666))
		if err != nil {
			if fdStderr != nil {
				fdStderr.Close()
			}
			runErr.Err = err
			errs <- runErr
			return
		}
		cmd.Stdout = fdStdout
		fdDep, err = os.OpenFile(fdDep.Name(), os.O_WRONLY|os.O_APPEND, os.FileMode(0o666))
		if err != nil {
			if fdStderr != nil {
				fdStderr.Close()
			}
			fdStdout.Close()
			runErr.Err = err
			errs <- runErr
			return
		}
		fdDepOpened = true
		fdDepW.Reset(fdDep)
		cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep)
		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvDepFd, 3+fdNum))
		fdNum++
		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvDepCwd, cwd))

		defer func() {
			if fdDepOpened {
				fdDep.Close()
			}
			fdStdout.Close()
			if fdStderr != nil {
				fdStderr.Close()
				logRecPath := path.Join(redoDir, tgtT+LogRecSuffix)
				if fdStderr, err = os.OpenFile(
					logRecPath,
					os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
					os.FileMode(0o666),
				); err == nil {
					fields := []recfile.Field{
						{F: "Build", V: BuildUUID.String()},
						{F: "PPID", V: strconv.Itoa(os.Getpid())},
						{F: "Cwd", V: cwd},
					}
					if cmd.Process != nil {
						fields = append(fields, recfile.Field{
							F: "PID", V: strconv.Itoa(cmd.Process.Pid),
						})
					}
					var ts tai64n.TAI64N
					ts.FromTime(started)
					fields = append(fields,
						recfile.Field{F: "Started", V: ts.Encode()},
					)
					ts.FromTime(finished)
					fields = append(fields,
						recfile.Field{F: "Finished", V: ts.Encode()})
					fields = append(fields, recfile.Field{
						F: "Duration",
						V: strconv.FormatInt(finished.Sub(started).Nanoseconds(), 10),
					})
					fields = append(fields, recfile.Field{F: "Cmd", V: cmdName})
					for _, arg := range args {
						fields = append(fields, recfile.Field{F: "Arg", V: arg})
					}
					for _, env := range cmd.Env {
						fields = append(fields, recfile.Field{F: "Env", V: env})
					}
					if exitErr != nil {
						fields = append(fields, recfile.Field{
							F: "ExitCode",
							V: strconv.Itoa(exitErr.ProcessState.ExitCode()),
						})
					}

					w := bufio.NewWriter(fdStderr)
					if !fdDepExists {
						var ifchanges []string
						ifchanges, err = depReadOnlyIfchanges(tgt.dep)
						if err != nil {
							err = ErrLine(err)
							goto Err
						}
						for _, ifchange := range ifchanges {
							fields = append(fields, recfile.Field{
								F: "Ifchange",
								V: ifchange,
							})
						}
					}
					_, err = recfile.NewWriter(w).WriteFields(fields...)
					if err != nil {
						err = ErrLine(err)
						goto Err
					}
					err = ErrLine(w.Flush())
				Err:
					if err != nil {
						log.Print(err)
						os.Remove(logRecPath)
					}
					fdStderr.Close()
				} else {
					log.Println("can not open", logRecPath, ":", err)
				}
			}
			lockRelease()
			if fdDepExists {
				os.Remove(fdDep.Name())
			}
			os.Remove(fdStdout.Name())
			os.Remove(tmpPath)
			{
				TmpsToCleanM.Lock()
				if fdDepExists {
					delete(TmpsToClean, fdDep.Name())
				}
				delete(TmpsToClean, fdStdout.Name())
				delete(TmpsToClean, tmpPath)
				TmpsToCleanM.Unlock()
			}
			if FdStatus != nil {
				if _, err = FdStatus.Write([]byte{StatusDone}); err != nil {
					log.Fatal(err)
				}
			}
			Jobs.Done()
		}()
		stderr, err := cmd.StderrPipe()
		if err != nil {
			runErr.Err = err
			errs <- runErr
			return
		}
		started = time.Now()
		err = cmd.Start()
		if err != nil {
			runErr.Err = err
			errs <- runErr
			return
		}
		RunningProcsM.Lock()
		RunningProcs[cmd.Process.Pid] = cmd.Process
		RunningProcsM.Unlock()
		pid := fmt.Sprintf("[%d]", cmd.Process.Pid)
		tracef(CDebug, "%s: runs %s", tgt, pid)

		stderrTerm := make(chan struct{})
		go func() {
			scanner := bufio.NewScanner(stderr)
			var line string
			var ts tai64n.TAI64N
			for scanner.Scan() {
				line = scanner.Text()
				if strings.HasPrefix(line, childStderrPrefix) {
					line = line[len(childStderrPrefix):]
					if StderrPrefix == "" {
						withPrependedTgt(line)
					} else {
						stderrWrite(StderrPrefix + line + "\n")
					}
					continue
				}
				if fdStderr != nil {
					ts.FromTime(time.Now())
					fmt.Fprintln(fdStderr, ts.Encode(), line)
				}
				if StderrSilent {
					continue
				}
				if MyPID != 0 {
					line = pid + " " + line
				}
				if StderrPrefix == "" {
					withPrependedTgt("[" + tgt.rel + "]" + line)
				} else {
					stderrWrite(StderrPrefix + "[" + tgt.rel + "]" + line + "\n")
				}
			}
			close(stderrTerm)
		}()

		// Wait for job completion
		<-stderrTerm
		err = cmd.Wait()
		RunningProcsM.Lock()
		delete(RunningProcs, cmd.Process.Pid)
		RunningProcsM.Unlock()
		finished = time.Now()
		runErr.Finished = &finished
		if err != nil {
			exitErr = err.(*exec.ExitError)
			runErr.Err = err
			errs <- runErr
			return
		}

		// Was $1 touched?
		if inode, errInode := inodeFromFileByPath(tgt.a); errInode == nil {
			if inodePrev == nil {
				runErr.Err = Err1WasTouched
				errs <- runErr
				return
			}
			if !inode.Equals(inodePrev) {
				runErr.Err = Err1WasTouched
				errs <- runErr
				return
			}
		}

		// Does it produce both stdout and tmp?
		fiStdout, err := os.Stat(fdStdout.Name())
		if err != nil {
			runErr.Err = ErrLine(err)
			errs <- runErr
			return
		}
		tmpExists := false
		_, err = os.Stat(tmpPath)
		if err == nil {
			if fiStdout.Size() > 0 {
				runErr.Err = errors.New("created both tmp and stdout")
				errs <- runErr
				return
			}
			tmpExists = true
		} else if !errors.Is(err, fs.ErrNotExist) {
			runErr.Err = ErrLine(err)
			errs <- runErr
			return
		}

		// Determine what file we must process at last
		var fd *os.File
		var chmod fs.FileMode
		if tmpExists {
			fd, err = os.Open(tmpPath)
			if err != nil {
				err = ErrLine(err)
				goto Finish
			}
			defer fd.Close()
			if fi, rerr := fd.Stat(); rerr == nil {
				chmod = fi.Mode()
			} else {
				err = ErrLine(rerr)
				goto Finish
			}
		} else if fiStdout.Size() > 0 {
			fd = fdStdout
		}

		// Do we need to ifcreate it, or ifchange with renaming?
		if fd == nil {
			if Level > 0 {
				tracef(CWarn, "%s: nothing written to target", tgt)
			}
			os.Remove(tgt.a)
			err = ifcreate(fdDepW, fdDep.Name(), tgtT)
			if err != nil {
				err = ErrLine(err)
				goto Finish
			}
		} else {
			var hsh Hash
			if hshPrev != HashNull {
				_, err = fd.Seek(0, io.SeekStart)
				if err != nil {
					err = ErrLine(err)
					goto Finish
				}
				hsh, err = fileHash(fd)
				if err != nil {
					err = ErrLine(err)
					goto Finish
				}
				if hsh == hshPrev {
					tracef(CDebug, "%s: has same hash, not renaming", tgt)
					err = ErrLine(os.Remove(fd.Name()))
					if err != nil {
						goto Finish
					}
					if chmod != 0 {
						err = ErrLine(os.Chmod(tgt.a, chmod))
						if err != nil {
							goto Finish
						}
					}
					err = ErrLine(os.Chtimes(tgt.a, finished, finished))
					if err != nil {
						goto Finish
					}
					if !NoSync {
						err = ErrLine(syncDir(tgtH))
						if err != nil {
							goto Finish
						}
					}
					err = ErrLine(depWrite(fdDepW, fdDep.Name(), tgtH, tgt, hsh))
					if err != nil {
						goto Finish
					}
					goto RecCommit
				}
			}
			if !NoSync {
				err = ErrLine(fd.Sync())
				if err != nil {
					goto Finish
				}
			}
			err = ErrLine(os.Rename(fd.Name(), tgt.a))
			if err != nil {
				goto Finish
			}
			if !NoSync {
				err = ErrLine(syncDir(tgtH))
				if err != nil {
					goto Finish
				}
			}
			err = ErrLine(depWrite(fdDepW, fdDep.Name(), tgtH, tgt, hsh))
			if err != nil {
				goto Finish
			}
		}

	RecCommit:
		// Commit .dep
		err = ErrLine(fdDepW.Flush())
		if err != nil {
			goto Finish
		}
		if !NoSync {
			err = ErrLine(fdDep.Sync())
			if err != nil {
				goto Finish
			}
		}
		err = ErrLine(os.Rename(fdDep.Name(), tgt.dep))
		if err != nil {
			goto Finish
		}
		{
			TmpsToCleanM.Lock()
			delete(TmpsToClean, fdDep.Name())
			TmpsToCleanM.Unlock()
		}
		fdDepExists = false
		if !NoSync {
			err = ErrLine(syncDir(redoDir))
			if err != nil {
				goto Finish
			}
		}
		fdDep.Close()
		fdDepOpened = false

		// Post-commit .dep sanitizing
		dep, err = depRead(tgt)
		if err == nil {
			ifchangeSeen := make(map[string]struct{}, len(dep.ifchanges))
			for _, ifchange := range dep.ifchanges {
				ifchangeSeen[ifchange.tgt.a] = struct{}{}
			}
			for _, ifcreate := range dep.ifcreates {
				if _, exists := ifchangeSeen[ifcreate.a]; exists {
					tracef(CWarn, "%s: simultaneous ifcreate and ifchange records", tgt)
				}
			}
		} else if errors.Is(err, fs.ErrNotExist) {
			err = nil
		} else {
			err = ErrLine(err)
			goto Finish
		}

	Finish:
		runErr.Err = err
		errs <- runErr
	}()
	return nil
}

func isOkRun(err error) bool {
	if err == nil {
		return true
	}
	var runErr RunError
	if errors.As(err, &runErr) && runErr.Err == nil {
		tracef(CRedo, "%s", runErr.Name())
		return true
	}
	tracef(CErr, "%s", err)
	return false
}
