// goredo -- djb's redo implementation on pure Go // Copyright (C) 2020-2024 Sergey Matveev // // 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 . package main import ( "bufio" "bytes" "encoding/hex" "errors" "flag" "fmt" "io" "io/fs" "log" "os" "os/signal" "path" "runtime" "sort" "strconv" "syscall" "github.com/google/uuid" "go.cypherpunks.ru/recfile" "golang.org/x/sys/unix" ) const ( CmdNameGoredo = "goredo" CmdNameRedo = "redo" CmdNameRedoAffects = "redo-affects" CmdNameRedoAlways = "redo-always" CmdNameRedoCleanup = "redo-cleanup" CmdNameRedoDep2Rec = "redo-dep2rec" CmdNameRedoDepFix = "redo-depfix" CmdNameRedoDot = "redo-dot" CmdNameRedoIfchange = "redo-ifchange" CmdNameRedoIfcreate = "redo-ifcreate" CmdNameRedoInode = "redo-inode" CmdNameRedoLog = "redo-log" CmdNameRedoOOD = "redo-ood" CmdNameRedoSources = "redo-sources" CmdNameRedoStamp = "redo-stamp" CmdNameRedoTargets = "redo-targets" CmdNameRedoWhichdo = "redo-whichdo" ) var ( Cwd string BuildUUID uuid.UUID IsTopRedo bool // is it the top redo instance UmaskCur int ) func mustSetenv(key string) { if err := os.Setenv(key, "1"); err != nil { panic(err) } } func mustParseFd(v, name string) *os.File { ptr, err := strconv.ParseUint(v, 10, 64) if err != nil { panic(err) } fd := os.NewFile(uintptr(ptr), name) if fd == nil { panic("can not parse fd: " + name) } return fd } func CmdName() string { return path.Base(os.Args[0]) } func main() { version := flag.Bool("version", false, "print version") warranty := flag.Bool("warranty", false, "print warranty information") var symlinks *bool cmdName := CmdName() if cmdName == CmdNameGoredo { symlinks = flag.Bool("symlinks", false, "create necessary symlinks in current directory") } flag.Usage = func() { usage(os.Args[0]) } BuildUUIDStr := os.Getenv(EnvBuildUUID) IsTopRedo = BuildUUIDStr == "" var args []string if IsTopRedo { flag.Parse() args = flag.Args() } else { args = os.Args[1:] } if *warranty { fmt.Println(Warranty) return } if *version { fmt.Println("goredo", Version, "built with", runtime.Version()) return } if cmdName == CmdNameGoredo && *symlinks { rc := 0 for _, cmdName := range []string{ CmdNameRedo, CmdNameRedoAffects, CmdNameRedoAlways, CmdNameRedoCleanup, CmdNameRedoDep2Rec, CmdNameRedoDepFix, CmdNameRedoDot, CmdNameRedoIfchange, CmdNameRedoIfcreate, CmdNameRedoInode, CmdNameRedoLog, CmdNameRedoOOD, CmdNameRedoSources, CmdNameRedoStamp, CmdNameRedoTargets, CmdNameRedoWhichdo, } { fmt.Println(os.Args[0], "<-", cmdName) if err := os.Symlink(os.Args[0], cmdName); err != nil { rc = 1 log.Print(err) } } os.Exit(rc) } log.SetFlags(log.Lshortfile) UmaskCur = syscall.Umask(0) syscall.Umask(UmaskCur) var err error Cwd, err = os.Getwd() if err != nil { log.Fatal(err) } TopDir = os.Getenv(EnvTopDir) if TopDir == "" { TopDir = "/" } else { TopDir = mustAbs(TopDir) } DirPrefix = os.Getenv(EnvDirPrefix) DepCwd = os.Getenv(EnvDepCwd) if flagStderrKeep != nil && *flagStderrKeep { mustSetenv(EnvStderrKeep) } if flagStderrSilent != nil && *flagStderrSilent { mustSetenv(EnvStderrSilent) } if flagNoProgress != nil && *flagNoProgress { mustSetenv(EnvNoProgress) } if flagDebug != nil && *flagDebug { mustSetenv(EnvDebug) } if flagLogWait != nil && *flagLogWait { mustSetenv(EnvLogWait) } if flagLogLock != nil && *flagLogLock { mustSetenv(EnvLogLock) } if flagLogPid != nil && *flagLogPid { mustSetenv(EnvLogPid) } if flagLogJS != nil && *flagLogJS { mustSetenv(EnvLogJS) } 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() } var traced bool if flagTraceAll != nil && *flagTraceAll { mustSetenv(EnvTrace) } if os.Getenv(EnvTrace) == "1" { TracedAll = true traced = true } else if flagTrace != nil { traced = *flagTrace } NoColor = os.Getenv(EnvNoColor) != "" NoSync = os.Getenv(EnvNoSync) == "1" StopIfMod = os.Getenv(EnvStopIfMod) == "1" switch s := os.Getenv(EnvInodeTrust); s { case "none": InodeTrust = InodeTrustNone case "", "ctime": InodeTrust = InodeTrustCtime case "mtime": InodeTrust = InodeTrustMtime default: log.Fatalln("unknown", EnvInodeTrust, "value") } // Those are internal envs FdOODTgts, err = os.CreateTemp("", "ood-tgts") if err != nil { log.Fatal(err) } if err = os.Remove(FdOODTgts.Name()); err != nil { log.Fatal(err) } FdOODTgtsLock, err = os.CreateTemp("", "ood-tgts.lock") if err != nil { log.Fatal(err) } if err = os.Remove(FdOODTgtsLock.Name()); err != nil { log.Fatal(err) } var fdLock *os.File if v := os.Getenv(EnvOODTgtsFd); v != "" { fd := mustParseFd(v, EnvOODTgtsFd) fdLock = mustParseFd(v, EnvOODTgtsLockFd) defer fdLock.Close() flock := unix.Flock_t{ Type: unix.F_WRLCK, Whence: io.SeekStart, } if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLKW, &flock); err != nil { log.Fatal(err) } if _, err = fd.Seek(0, io.SeekStart); err != nil { log.Fatal(err) } tgtsRaw, err := io.ReadAll(bufio.NewReader(fd)) if err != nil { log.Fatal(err) } flock.Type = unix.F_UNLCK if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLK, &flock); err != nil { log.Fatal(err) } OODTgts = make(map[string]struct{}) for _, tgtRaw := range bytes.Split(tgtsRaw, []byte{0}) { t := string(tgtRaw) if t == "" { continue } OODTgts[t] = struct{}{} tracef(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) } if Level < 0 { panic("negative " + EnvLevel) } } var fdDep *os.File if v := os.Getenv(EnvDepFd); v != "" { fdDep = mustParseFd(v, EnvDepFd) } tgts := make([]*Tgt, 0, len(args)) for _, arg := range args { tgts = append(tgts, NewTgt(arg)) } tgtsWasEmpty := len(tgts) == 0 if BuildUUIDStr == "" { BuildUUID = uuid.New() if tgtsWasEmpty { tgts = append(tgts, NewTgt("all")) } tracef(CDebug, "inode-trust: %s", InodeTrust) } else { BuildUUID, err = uuid.Parse(BuildUUIDStr) if err != nil { log.Fatal(err) } } if cmdName == CmdNameRedo { statusInit() } killed := make(chan os.Signal, 1) signal.Notify(killed, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) go func() { <-killed tracef(CDebug, "[%s] killed", BuildUUID) jsReleaseAll() RunningProcsM.Lock() for pid, proc := range RunningProcs { tracef(CDebug, "[%s] killing child %d", BuildUUID, pid) _ = proc.Signal(syscall.SIGTERM) } os.Exit(1) }() ok := true err = nil tracef(CDebug, "[%s] run: %s %s cwd:%s dirprefix:%s", BuildUUID, cmdName, tgts, Cwd, DirPrefix) switch cmdName { case CmdNameRedo: for _, tgt := range tgts { ok, err = ifchange([]*Tgt{tgt}, true, traced) if err != nil || !ok { break } } case CmdNameRedoIfchange: ok, err = ifchange(tgts, *flagForcedIfchange, traced) if err == nil { err = depsWrite(fdDep, tgts) } case CmdNameRedoIfcreate: if fdDep == nil { log.Fatalln("no", EnvDepFd) } fdDepW := bufio.NewWriter(fdDep) for _, tgt := range tgts { err = ifcreate(fdDepW, fdDep.Name(), tgt.RelTo(path.Join(Cwd, DirPrefix))) if err != nil { break } } err = fdDepW.Flush() case CmdNameRedoAlways: if fdDep == nil { log.Fatalln("no", EnvDepFd) } err = always(fdDep, fdDep.Name()) case CmdNameRedoCleanup: for _, what := range tgts { err = cleanupWalker(Cwd, path.Base(what.a)) if err != nil { break } } case CmdNameRedoDot: err = dotPrint(tgts) case CmdNameRedoStamp: if fdDep == nil { log.Fatalln("no", EnvDepFd) } var hsh Hash hsh, err = fileHash(os.Stdin) if err != nil { break } err = stamp(fdDep, fdDep.Name(), hsh) case CmdNameRedoLog: if len(tgts) != 1 { log.Fatal("single target expected") } err = showBuildLog(tgts[0], nil, 0) case CmdNameRedoWhichdo: if len(tgts) != 1 { log.Fatal("single target expected") } var dos []string dos, err = whichdo(tgts[0]) if err != nil { if errors.Is(err, fs.ErrNotExist) { err = nil ok = false } else { break } } for _, do := range dos { fmt.Println(do) } case CmdNameRedoTargets: raws := make([]string, 0, len(tgts)) for _, tgt := range tgts { raws = append(raws, tgt.rel) } if tgtsWasEmpty { raws = []string{Cwd} } raws, err = targetsWalker(raws) if err != nil { err = ErrLine(err) break } sort.Strings(raws) for _, tgt := range raws { fmt.Println(tgt) } case CmdNameRedoAffects: if tgtsWasEmpty { log.Fatal("no targets specified") } var res []string { var tgtsKnown []string tgtsKnown, err = targetsWalker([]string{Cwd}) if err != nil { err = ErrLine(err) break } deps := make(map[string]map[string]*Tgt) for _, tgt := range tgtsKnown { collectDeps(NewTgt(tgt), 0, deps, true) } seen := make(map[string]*Tgt) for _, tgt := range tgts { collectWholeDeps(deps[tgt.rel], deps, seen) } res = make([]string, 0, len(seen)) for _, dep := range seen { res = append(res, dep.rel) } } sort.Strings(res) for _, dep := range res { fmt.Println(dep) } case CmdNameRedoOOD: raws := make([]string, 0, len(tgts)) for _, tgt := range tgts { raws = append(raws, tgt.rel) } if tgtsWasEmpty { raws, err = targetsWalker([]string{Cwd}) if err != nil { break } } sort.Strings(raws) var ood bool for _, tgt := range raws { ood, err = isOOD(NewTgt(tgt), 0, nil) if err != nil { err = ErrLine(err) break } if ood { fmt.Println(tgt) } } case CmdNameRedoSources: srcs := make(map[string]*Tgt) { raws := make([]string, 0, len(tgts)) for _, tgt := range tgts { raws = append(raws, tgt.rel) } if tgtsWasEmpty { raws, err = targetsWalker([]string{Cwd}) if err != nil { err = ErrLine(err) break } } sort.Strings(raws) tgts = tgts[:0] for _, raw := range raws { tgts = append(tgts, NewTgt(raw)) } seen := make(map[string]struct{}) seenDeps := make(map[string]struct{}) err = ErrLine(sourcesWalker(tgts, seen, seenDeps, srcs)) } if err != nil { break } res := make([]string, 0, len(srcs)) for _, tgt := range srcs { res = append(res, tgt.rel) } srcs = nil sort.Strings(res) for _, src := range res { fmt.Println(src) } case CmdNameRedoDepFix: IfchangeCache = nil DepFixHashCache = make(map[string]Hash) err = depFix(Cwd) case CmdNameRedoInode: var inode *Inode for _, tgt := range tgts { inode, err = inodeFromFileByPath(tgt.a) if err != nil { err = ErrLine(err) break } err = recfileWrite(os.Stdout, append( []recfile.Field{{Name: "Target", Value: tgt.String()}}, inode.RecfileFields()...)...) if err != nil { err = ErrLine(err) break } } case CmdNameRedoDep2Rec: var data []byte data, err = os.ReadFile(tgts[0].a) if err != nil { break } var build uuid.UUID build, data, err = depHeadParse(data) if err != nil { break } w := bufio.NewWriter(os.Stdout) err = recfileWrite(w, []recfile.Field{ {Name: "Build", Value: build.String()}, }...) if err != nil { break } var typ byte var chunk []byte var inode Inode for len(data) > 0 { typ, chunk, data, _ = chunkRead(data) switch typ { case DepTypeAlways: err = recfileWrite(w, []recfile.Field{ {Name: "Type", Value: "always"}, }...) case DepTypeStamp: err = recfileWrite(w, []recfile.Field{ {Name: "Type", Value: "stamp"}, {Name: "Hash", Value: hex.EncodeToString(chunk)}, }...) case DepTypeIfcreate: err = recfileWrite(w, []recfile.Field{ {Name: "Type", Value: "ifcreate"}, {Name: "Target", Value: string(chunk)}, }...) case DepTypeIfchange: name := string(chunk[InodeLen+HashLen:]) meta := chunk[:InodeLen+HashLen] fields := []recfile.Field{ {Name: "Type", Value: "ifchange"}, {Name: "Target", Value: name}, } fields = append(fields, recfile.Field{ Name: "Hash", Value: Hash(meta[InodeLen:]).String(), }) inode = Inode(meta[:][:InodeLen]) fields = append(fields, inode.RecfileFields()...) err = recfileWrite(w, fields...) case DepTypeIfchangeNonex: err = recfileWrite(w, []recfile.Field{ {Name: "Type", Value: "ifchange"}, {Name: "Target", Value: string(chunk)}, }...) } if err != nil { break } } err = w.Flush() default: log.Fatalln("unknown command", cmdName) } if err != nil { log.Print(err) } rc := 0 if !ok || err != nil { rc = 1 } tracef(CDebug, "[%s] finished: %s %s", BuildUUID, cmdName, tgts) os.Exit(rc) }