]> Cypherpunks.ru repositories - goredo.git/blobdiff - main.go
default.do-s must not depend on themselves
[goredo.git] / main.go
diff --git a/main.go b/main.go
index df9296f74b16875fb103947a3d5956cdebfec268..c3b1be78677b731291db45ca8af4cfab7b74a5d3 100644 (file)
--- a/main.go
+++ b/main.go
@@ -1,6 +1,6 @@
 /*
-goredo -- redo implementation on pure Go
-Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
+goredo -- djb's redo implementation on pure Go
+Copyright (C) 2020-2021 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
@@ -18,6 +18,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 package main
 
 import (
+       "bufio"
+       "bytes"
        "crypto/rand"
        "flag"
        "fmt"
@@ -25,42 +27,39 @@ import (
        "io/ioutil"
        "log"
        "os"
+       "os/signal"
        "path"
        "path/filepath"
+       "runtime"
+       "sort"
        "strconv"
-       "sync"
+       "syscall"
 
        "go.cypherpunks.ru/recfile"
        "golang.org/x/sys/unix"
 )
 
 const (
-       RedoLevelEnv        = "REDO_LEVEL"
-       RedoDepFdEnv        = "REDO_DEP_FD"
-       RedoDirPrefixEnv    = "REDO_DIRPREFIX"
-       RedoBuildUUIDEnv    = "REDO_BUILD_UUID"
-       RedoStderrPrefixEnv = "REDO_STDERR_PREFIX"
-       RedoJSRFdEnv        = "REDO_JSR_FD"
-       RedoJSWFdEnv        = "REDO_JSW_FD"
+       CmdNameGoredo       = "goredo"
+       CmdNameRedo         = "redo"
+       CmdNameRedoAffects  = "redo-affects"
+       CmdNameRedoAlways   = "redo-always"
+       CmdNameRedoCleanup  = "redo-cleanup"
+       CmdNameRedoDot      = "redo-dot"
+       CmdNameRedoIfchange = "redo-ifchange"
+       CmdNameRedoIfcreate = "redo-ifcreate"
+       CmdNameRedoLog      = "redo-log"
+       CmdNameRedoOOD      = "redo-ood"
+       CmdNameRedoSources  = "redo-sources"
+       CmdNameRedoStamp    = "redo-stamp"
+       CmdNameRedoTargets  = "redo-targets"
+       CmdNameRedoWhichdo  = "redo-whichdo"
 )
 
 var (
-       Level       = 0
-       Trace  bool = false
-       Force  bool = false
-       NoHash bool = false
-       NoSync bool = false
-       Cwd    string
-       Jobs   sync.WaitGroup
-
-       JobsN = flag.Uint("j", 1, "number of parallel jobs (0 for infinite)")
-
-       StderrKeep   bool = false
-       StderrSilent bool = false
-       StderrPrefix string
-
+       Cwd       string
        BuildUUID string
-       IsMaster  bool = false
+       IsTopRedo bool // is it the top redo instance
 )
 
 func mustSetenv(key, value string) {
@@ -81,250 +80,308 @@ func mustParseFd(v, name string) *os.File {
        return fd
 }
 
-func ifchange(tgts []string) (bool, error) {
-       jsInit()
-       defer Jobs.Wait()
-       defer jsAcquire()
-       errs := make(chan error, len(tgts))
-       jobs := 0
-       ok := true
-       var err error
-       for _, tgt := range tgts {
-               var ood bool
-               if Force {
-                       ood = true
-               } else {
-                       ood, err = isOOD(Cwd, tgt, 0)
-                       if err != nil {
-                               return false, err
-                       }
-               }
-               if !ood {
-                       continue
-               }
-               if isSrc(Cwd, tgt) {
-                       trace(CDebug, "%s is source", tgt)
-                       continue
-               }
-               if err = runScript(tgt, errs); err != nil {
-                       return false, err
-               }
-               if Force {
-                       // Sequentially run jobs
-                       err = <-errs
-                       Jobs.Wait()
-                       if err != nil {
-                               trace(CErr, "%s", err)
-                               return false, nil
-                       }
-                       continue
-               }
-               jobs++
-       }
-       for i := 0; i < jobs; i++ {
-               if err = <-errs; err != nil {
-                       ok = false
-                       trace(CErr, "%s", err)
-               }
-       }
-       return ok, nil
+func CmdName() string {
+       return path.Base(os.Args[0])
 }
 
 func main() {
-       xflag := flag.Bool("x", false, "trace current target (sh -x) (set REDO_TRACE=1 for others too)")
-       stderrKeep := flag.Bool("stderr-keep", false, "keep job's stderr (REDO_STDERR_KEEP=1)")
-       stderrSilent := flag.Bool("stderr-silent", false, "do not print job's stderr (REDO_STDERR_SILENT=1)")
-       debug := flag.Bool("debug", false, "enable debug logging (REDO_DEBUG=1)")
-       logDone := flag.Bool("log-done", false, "enable done messages logging (REDO_LOG_DONE=1)")
-       logLock := flag.Bool("log-lock", false, "enable lock messages logging (REDO_LOG_LOCK=1)")
-       logPid := flag.Bool("log-pid", false, "append PIDs (REDO_LOG_PID=1)")
-       logJS := flag.Bool("log-js", false, "enable jobserver messages logging (REDO_LOG_JS=1)")
        version := flag.Bool("version", false, "print version")
        warranty := flag.Bool("warranty", false, "print warranty information")
-       symlinks := flag.Bool("symlinks", false, "create necessary symlinks in current direcotyr")
 
-       flag.Usage = usage
+       var symlinks *bool
+       cmdName := CmdName()
+       if cmdName == "goredo" {
+               symlinks = flag.Bool("symlinks", false, "create necessary symlinks in current directory")
+       }
+
+       flag.Usage = func() { usage(os.Args[0]) }
        flag.Parse()
        if *warranty {
                fmt.Println(Warranty)
                return
        }
        if *version {
-               fmt.Println(versionGet())
+               fmt.Println("goredo", Version, "built with", runtime.Version())
                return
        }
-       if *symlinks {
+       if cmdName == CmdNameGoredo && *symlinks {
+               rc := 0
                for _, cmdName := range []string{
-                       "redo",
-                       "redo-always",
-                       "redo-cleanup",
-                       "redo-ifchange",
-                       "redo-ifcreate",
-                       "redo-log",
-                       "redo-stamp",
-                       "redo-whichdo",
+                       CmdNameRedo,
+                       CmdNameRedoAffects,
+                       CmdNameRedoAlways,
+                       CmdNameRedoCleanup,
+                       CmdNameRedoDot,
+                       CmdNameRedoIfchange,
+                       CmdNameRedoIfcreate,
+                       CmdNameRedoLog,
+                       CmdNameRedoOOD,
+                       CmdNameRedoSources,
+                       CmdNameRedoStamp,
+                       CmdNameRedoTargets,
+                       CmdNameRedoWhichdo,
                } {
+                       fmt.Println(os.Args[0], "<-", cmdName)
                        if err := os.Symlink(os.Args[0], cmdName); err != nil {
-                               log.Fatalln(err)
+                               rc = 1
+                               log.Println(err)
                        }
                }
-               return
+               os.Exit(rc)
        }
-       log.SetFlags(0)
+       log.SetFlags(log.Lshortfile)
 
        var err error
-       Cwd, err = unix.Getwd()
+       Cwd, err = os.Getwd()
        if err != nil {
-               panic(err)
+               log.Fatalln(err)
        }
 
-       NoColor = os.Getenv("NO_COLOR") != ""
-       NoHash = os.Getenv("REDO_NO_HASH") == "1"
-       NoSync = os.Getenv("REDO_NO_SYNC") == "1"
+       NoColor = os.Getenv(EnvNoColor) != ""
+       NoSync = os.Getenv(EnvNoSync) == "1"
+       InodeTrust = os.Getenv(EnvInodeNoTrust) == ""
 
-       TopDir = os.Getenv("REDO_TOP_DIR")
-       if TopDir != "" {
+       TopDir = os.Getenv(EnvTopDir)
+       if TopDir == "" {
+               TopDir = "/"
+       } else {
                TopDir, err = filepath.Abs(TopDir)
                if err != nil {
                        panic(err)
                }
        }
+       DirPrefix = os.Getenv(EnvDirPrefix)
+       DepCwd = os.Getenv(EnvDepCwd)
 
-       if *stderrKeep {
-               mustSetenv("REDO_STDERR_KEEP", "1")
+       if flagStderrKeep != nil && *flagStderrKeep {
+               mustSetenv(EnvStderrKeep, "1")
        }
-       if *stderrSilent {
-               mustSetenv("REDO_STDERR_SILENT", "1")
+       if flagStderrSilent != nil && *flagStderrSilent {
+               mustSetenv(EnvStderrSilent, "1")
        }
-       if *debug {
-               mustSetenv("REDO_DEBUG", "1")
+       if flagNoProgress != nil && *flagNoProgress {
+               mustSetenv(EnvNoProgress, "1")
        }
-       if *logDone {
-               mustSetenv("REDO_LOG_DONE", "1")
+       if flagDebug != nil && *flagDebug {
+               mustSetenv(EnvDebug, "1")
        }
-       if *logLock {
-               mustSetenv("REDO_LOG_LOCK", "1")
+       if flagLogWait != nil && *flagLogWait {
+               mustSetenv(EnvLogWait, "1")
        }
-       if *logPid {
-               mustSetenv("REDO_LOG_PID", "1")
+       if flagLogLock != nil && *flagLogLock {
+               mustSetenv(EnvLogLock, "1")
        }
-       if *logJS {
-               mustSetenv("REDO_LOG_JS", "1")
+       if flagLogPid != nil && *flagLogPid {
+               mustSetenv(EnvLogPid, "1")
        }
-       StderrKeep = os.Getenv("REDO_STDERR_KEEP") == "1"
-       StderrSilent = os.Getenv("REDO_STDERR_SILENT") == "1"
-       Debug = os.Getenv("REDO_DEBUG") == "1"
-       LogDone = os.Getenv("REDO_LOG_DONE") == "1"
-       LogLock = os.Getenv("REDO_LOG_LOCK") == "1"
-       LogJS = os.Getenv("REDO_LOG_JS") == "1"
-       if Debug || os.Getenv("REDO_LOG_PID") == "1" {
+       if flagLogJS != nil && *flagLogJS {
+               mustSetenv(EnvLogJS, "1")
+       }
+       StderrKeep = os.Getenv(EnvStderrKeep) == "1"
+       StderrSilent = os.Getenv(EnvStderrSilent) == "1"
+       NoProgress = os.Getenv(EnvNoProgress) == "1"
+       Debug = os.Getenv(EnvDebug) == "1"
+       LogWait = os.Getenv(EnvLogWait) == "1"
+       LogLock = os.Getenv(EnvLogLock) == "1"
+       LogJS = os.Getenv(EnvLogJS) == "1"
+       if Debug || os.Getenv(EnvLogPid) == "1" {
                MyPid = os.Getpid()
        }
-       if *xflag {
-               Trace = true
-       } else {
-               Trace = os.Getenv("REDO_TRACE") == "1"
+       var traced bool
+       if flagTraceAll != nil && *flagTraceAll {
+               mustSetenv(EnvTrace, "1")
+       }
+       if os.Getenv(EnvTrace) == "1" {
+               TracedAll = true
+               traced = true
+       } else if flagTrace != nil {
+               traced = *flagTrace
        }
 
        // Those are internal envs
-       StderrPrefix = os.Getenv(RedoStderrPrefixEnv)
-       if v := os.Getenv(RedoLevelEnv); v != "" {
-               level, err := strconv.ParseUint(v, 10, 64)
+       FdOODTgts, err = ioutil.TempFile("", "ood-tgts")
+       if err != nil {
+               log.Fatalln(err)
+       }
+       if err = os.Remove(FdOODTgts.Name()); err != nil {
+               log.Fatalln(err)
+       }
+       FdOODTgtsLock, err = ioutil.TempFile("", "ood-tgts.lock")
+       if err != nil {
+               log.Fatalln(err)
+       }
+       if err = os.Remove(FdOODTgtsLock.Name()); err != nil {
+               log.Fatalln(err)
+       }
+
+       if v := os.Getenv(EnvOODTgtsFd); v != "" {
+               fd := mustParseFd(v, EnvOODTgtsFd)
+               fdLock := mustParseFd(v, EnvOODTgtsLockFd)
+               if err = unix.Flock(int(fdLock.Fd()), unix.LOCK_EX); err != nil {
+                       log.Fatalln(err)
+               }
+               if _, err = fd.Seek(0, io.SeekStart); err != nil {
+                       log.Fatalln(err)
+               }
+               tgtsRaw, err := ioutil.ReadAll(bufio.NewReader(fd))
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               unix.Flock(int(fdLock.Fd()), unix.LOCK_UN)
+               OODTgts = map[string]struct{}{}
+               for _, tgtRaw := range bytes.Split(tgtsRaw, []byte{0}) {
+                       t := string(tgtRaw)
+                       if t == "" {
+                               continue
+                       }
+                       OODTgts[t] = struct{}{}
+                       trace(CDebug, "ood: known to be: %s", t)
+               }
+       }
+
+       StderrPrefix = os.Getenv(EnvStderrPrefix)
+       if v := os.Getenv(EnvLevel); v != "" {
+               Level, err = strconv.Atoi(v)
                if err != nil {
                        panic(err)
                }
-               Level = int(level)
+               if Level < 0 {
+                       panic("negative " + EnvLevel)
+               }
        }
+
        var fdDep *os.File
-       if v := os.Getenv(RedoDepFdEnv); v != "" {
-               fdDep = mustParseFd(v, RedoDepFdEnv)
+       if v := os.Getenv(EnvDepFd); v != "" {
+               fdDep = mustParseFd(v, EnvDepFd)
        }
-       BuildUUID = os.Getenv(RedoBuildUUIDEnv)
+
+       tgts := flag.Args()
+       BuildUUID = os.Getenv(EnvBuildUUID)
+       tgtsWasEmpty := len(tgts) == 0
        if BuildUUID == "" {
-               IsMaster = true
+               IsTopRedo = true
                raw := new([16]byte)
                if _, err = io.ReadFull(rand.Reader, raw[:]); err != nil {
-                       panic(err)
+                       log.Fatalln(err)
                }
                raw[6] = (raw[6] & 0x0F) | uint8(4<<4) // version 4
                BuildUUID = fmt.Sprintf(
                        "%x-%x-%x-%x-%x",
                        raw[0:4], raw[4:6], raw[6:8], raw[8:10], raw[10:],
                )
+               if tgtsWasEmpty {
+                       tgts = []string{"all"}
+               }
        }
 
-       tgts := flag.Args()
-       if len(tgts) == 0 {
-               tgts = []string{"all"}
+       if cmdName == CmdNameRedo || cmdName == CmdNameRedoIfchange {
+               statusInit()
+       }
+
+       for i, tgt := range tgts {
+               if path.IsAbs(tgt) {
+                       tgts[i] = cwdMustRel(tgt)
+               }
        }
+
+       killed := make(chan os.Signal, 0)
+       signal.Notify(killed, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
+       go func() {
+               <-killed
+               trace(CDebug, "[%s] killed", BuildUUID)
+               jsReleaseAll()
+               RunningProcsM.Lock()
+               for pid, proc := range RunningProcs {
+                       trace(CDebug, "[%s] killing child %d", BuildUUID, pid)
+                       proc.Signal(syscall.SIGTERM)
+               }
+               os.Exit(1)
+       }()
+
        ok := true
        err = nil
-       cmdName := path.Base(os.Args[0])
-       trace(CDebug, "[%s] run: %s %s [%s]", BuildUUID, cmdName, tgts, Cwd)
+       trace(
+               CDebug, "[%s] run: %s %s cwd:%s dirprefix:%s",
+               BuildUUID, cmdName, tgts, Cwd, DirPrefix,
+       )
 
 CmdSwitch:
        switch cmdName {
-       case "redo":
-               Force = true
-               ok, err = ifchange(tgts)
-       case "redo-ifchange":
-               ok, err = ifchange(tgts)
-               writeDeps(fdDep, tgts)
-       case "redo-ifcreate":
+       case CmdNameRedo:
+               for _, tgt := range tgts {
+                       ok, err = ifchange([]string{tgt}, true, traced)
+                       if err != nil || !ok {
+                               break
+                       }
+               }
+       case CmdNameRedoIfchange:
+               ok, err = ifchange(tgts, false, traced)
+               if err == nil {
+                       err = writeDeps(fdDep, tgts)
+               }
+       case CmdNameRedoIfcreate:
                if fdDep == nil {
-                       log.Fatalln("no", RedoDepFdEnv)
+                       log.Fatalln("no", EnvDepFd)
                }
                for _, tgt := range tgts {
-                       err = ifcreate(fdDep, tgt)
+                       tgtRel, err := filepath.Rel(
+                               filepath.Join(Cwd, DirPrefix),
+                               filepath.Join(Cwd, tgt),
+                       )
+                       if err != nil {
+                               break
+                       }
+                       err = ifcreate(fdDep, tgtRel)
                        if err != nil {
                                break
                        }
                }
-       case "redo-always":
+       case CmdNameRedoAlways:
                if fdDep == nil {
-                       log.Fatalln("no", RedoDepFdEnv)
+                       log.Fatalln("no", EnvDepFd)
                }
                err = always(fdDep)
-       case "redo-cleanup":
+       case CmdNameRedoCleanup:
                for _, what := range tgts {
                        err = cleanupWalker(Cwd, what)
                        if err != nil {
                                break
                        }
                }
-       case "redo-stamp":
+       case CmdNameRedoDot:
+               err = dotPrint(tgts)
+       case CmdNameRedoStamp:
                if fdDep == nil {
-                       log.Fatalln("no", RedoDepFdEnv)
+                       log.Fatalln("no", EnvDepFd)
                }
                err = stamp(fdDep, os.Stdin)
-       case "redo-log":
+       case CmdNameRedoLog:
                if len(tgts) != 1 {
                        log.Fatalln("single target expected")
                }
                d, t := cwdAndTgt(tgts[0])
-               var fd *os.File
-               fd, err = os.Open(path.Join(d, RedoDir, t+LogSuffix))
-               if err != nil {
-                       break
-               }
-               _, err = io.Copy(os.Stdout, fd)
-       case "redo-whichdo":
+               err = showBuildLog(d, t, nil, 0)
+       case CmdNameRedoWhichdo:
                if len(tgts) != 1 {
                        log.Fatalln("single target expected")
                }
                var fdTmp *os.File
-               fdTmp, err = ioutil.TempFile("", "")
+               fdTmp, err = ioutil.TempFile("", "whichdo")
                if err != nil {
                        break
                }
-               os.Remove(fdTmp.Name())
-               var doFile string
+               if err = os.Remove(fdTmp.Name()); err != nil {
+                       break
+               }
                cwd, tgt := cwdAndTgt(tgts[0])
-               doFile, _, err = findDo(fdTmp, cwd, tgt)
+               doFile, upLevels, err := findDo(fdTmp, cwd, tgt)
+               if err != nil {
+                       break
+               }
+               _, err = fdTmp.Seek(0, io.SeekStart)
                if err != nil {
                        break
                }
-               fdTmp.Seek(0, 0)
                r := recfile.NewReader(fdTmp)
                for {
                        m, err := r.NextMap()
@@ -334,20 +391,102 @@ CmdSwitch:
                                }
                                break CmdSwitch
                        }
-                       rel, err := filepath.Rel(Cwd, path.Join(cwd, m["Target"]))
+                       fmt.Println(cwdMustRel(cwd, m["Target"]))
+               }
+               if doFile == "" {
+                       ok = false
+               } else {
+                       p := make([]string, 0, upLevels+2)
+                       p = append(p, cwd)
+                       for i := 0; i < upLevels; i++ {
+                               p = append(p, "..")
+                       }
+                       p = append(p, doFile)
+                       rel, err := filepath.Rel(Cwd, path.Join(p...))
                        if err != nil {
                                panic(err)
                        }
                        fmt.Println(rel)
                }
-               ok = doFile != ""
+       case CmdNameRedoTargets:
+               if tgtsWasEmpty {
+                       tgts = []string{Cwd}
+               }
+               tgts, err = targetsWalker(tgts)
+               if err != nil {
+                       break
+               }
+               sort.Strings(tgts)
+               for _, tgt := range tgts {
+                       fmt.Println(tgt)
+               }
+       case CmdNameRedoAffects:
+               if tgtsWasEmpty {
+                       log.Fatalln("no targets specified")
+               }
+               var tgtsKnown []string
+               tgtsKnown, err = targetsWalker([]string{Cwd})
+               if err != nil {
+                       break
+               }
+               deps := map[string]map[string]struct{}{}
+               for _, tgt := range tgtsKnown {
+                       collectDeps(Cwd, tgt, 0, deps, true)
+               }
+               seen := map[string]struct{}{}
+               for _, tgt := range tgts {
+                       collectWholeDeps(deps[tgt], deps, seen)
+               }
+               tgts := make([]string, 0, len(seen))
+               for dep := range seen {
+                       tgts = append(tgts, dep)
+               }
+               sort.Strings(tgts)
+               for _, dep := range tgts {
+                       fmt.Println(dep)
+               }
+       case CmdNameRedoOOD:
+               if tgtsWasEmpty {
+                       tgts, err = targetsWalker([]string{Cwd})
+                       if err != nil {
+                               break
+                       }
+               }
+               sort.Strings(tgts)
+               var ood bool
+               for _, tgt := range tgts {
+                       ood, err = isOOD(Cwd, tgt, 0, nil)
+                       if err != nil {
+                               break
+                       }
+                       if ood {
+                               fmt.Println(tgt)
+                       }
+               }
+       case CmdNameRedoSources:
+               if tgtsWasEmpty {
+                       tgts, err = targetsWalker([]string{Cwd})
+                       if err != nil {
+                               break
+                       }
+               }
+               sort.Strings(tgts)
+               var srcs []string
+               srcs, err = sourcesWalker(tgts)
+               sort.Strings(srcs)
+               for _, src := range srcs {
+                       fmt.Println(src)
+               }
        default:
                log.Fatalln("unknown command", cmdName)
        }
        if err != nil {
                log.Println(err)
        }
+       rc := 0
        if !ok || err != nil {
-               os.Exit(1)
+               rc = 1
        }
+       trace(CDebug, "[%s] finished: %s %s", BuildUUID, cmdName, tgts)
+       os.Exit(rc)
 }