X-Git-Url: http://www.git.cypherpunks.ru/?a=blobdiff_plain;f=main.go;h=eb67c8069fa2534b645368e1504b8b6963bb7853;hb=refs%2Fheads%2Fmaster;hp=4de78be52fa786830cfbb92862c1c495cd59140a;hpb=314f58ec690c7321535d6718e8d3a0ecb4cac019;p=goredo.git diff --git a/main.go b/main.go index 4de78be..eb67c80 100644 --- a/main.go +++ b/main.go @@ -1,47 +1,72 @@ -/* -goredo -- djb's redo implementation on pure Go -Copyright (C) 2020-2021 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 . -*/ +// 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" - "crypto/rand" + "encoding/hex" + "errors" "flag" "fmt" "io" - "io/ioutil" + "io/fs" "log" "os" + "os/signal" "path" - "path/filepath" + "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 string + BuildUUID uuid.UUID + IsTopRedo bool // is it the top redo instance + UmaskCur int ) -func mustSetenv(key, value string) { - if err := os.Setenv(key, value); err != nil { +func mustSetenv(key string) { + if err := os.Setenv(key, "1"); err != nil { panic(err) } } @@ -58,88 +83,112 @@ func mustParseFd(v, name string) *os.File { 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") - symlinks := flag.Bool("symlinks", false, "create necessary symlinks in current directory") - flag.Usage = usage - flag.Parse() + 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(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-dot", - "redo-ifchange", - "redo-ifcreate", - "redo-log", - "redo-stamp", - "redo-whichdo", + 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.Println(err) + log.Print(err) } } os.Exit(rc) } - log.SetFlags(0) + log.SetFlags(log.Lshortfile) + + UmaskCur = syscall.Umask(0) + syscall.Umask(UmaskCur) var err error Cwd, err = os.Getwd() if err != nil { - log.Fatalln(err) + log.Fatal(err) } - NoColor = os.Getenv(EnvNoColor) != "" - NoSync = os.Getenv(EnvNoSync) == "1" - InodeTrust = os.Getenv(EnvInodeNoTrust) == "" - TopDir = os.Getenv(EnvTopDir) if TopDir == "" { TopDir = "/" } else { - TopDir, err = filepath.Abs(TopDir) - if err != nil { - panic(err) - } + TopDir = mustAbs(TopDir) } DirPrefix = os.Getenv(EnvDirPrefix) + DepCwd = os.Getenv(EnvDepCwd) - if *flagStderrKeep { - mustSetenv(EnvStderrKeep, "1") + if flagStderrKeep != nil && *flagStderrKeep { + mustSetenv(EnvStderrKeep) } - if *flagStderrSilent { - mustSetenv(EnvStderrSilent, "1") + if flagStderrSilent != nil && *flagStderrSilent { + mustSetenv(EnvStderrSilent) } - if *flagNoProgress { - mustSetenv(EnvNoProgress, "1") + if flagNoProgress != nil && *flagNoProgress { + mustSetenv(EnvNoProgress) } - if *flagDebug { - mustSetenv(EnvDebug, "1") + if flagDebug != nil && *flagDebug { + mustSetenv(EnvDebug) } - if *flagLogWait { - mustSetenv(EnvLogWait, "1") + if flagLogWait != nil && *flagLogWait { + mustSetenv(EnvLogWait) } - if *flagLogLock { - mustSetenv(EnvLogLock, "1") + if flagLogLock != nil && *flagLogLock { + mustSetenv(EnvLogLock) } - if *flagLogPid { - mustSetenv(EnvLogPid, "1") + if flagLogPid != nil && *flagLogPid { + mustSetenv(EnvLogPid) } - if *flagLogJS { - mustSetenv(EnvLogJS, "1") + if flagLogJS != nil && *flagLogJS { + mustSetenv(EnvLogJS) } StderrKeep = os.Getenv(EnvStderrKeep) == "1" StderrSilent = os.Getenv(EnvStderrSilent) == "1" @@ -149,48 +198,71 @@ func main() { LogLock = os.Getenv(EnvLogLock) == "1" LogJS = os.Getenv(EnvLogJS) == "1" if Debug || os.Getenv(EnvLogPid) == "1" { - MyPid = os.Getpid() + MyPID = os.Getpid() } var traced bool - if *flagTraceAll { - mustSetenv(EnvTrace, "1") + if flagTraceAll != nil && *flagTraceAll { + mustSetenv(EnvTrace) } - if *flagTrace { + if os.Getenv(EnvTrace) == "1" { + TracedAll = true traced = true - } else { - traced = os.Getenv(EnvTrace) == "1" + } 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 = ioutil.TempFile("", "ood-tgts") + FdOODTgts, err = os.CreateTemp("", "ood-tgts") if err != nil { - panic(err) + log.Fatal(err) } if err = os.Remove(FdOODTgts.Name()); err != nil { - panic(err) + log.Fatal(err) } - FdOODTgtsLock, err = ioutil.TempFile("", "ood-tgts.lock") + FdOODTgtsLock, err = os.CreateTemp("", "ood-tgts.lock") if err != nil { - panic(err) + log.Fatal(err) } if err = os.Remove(FdOODTgtsLock.Name()); err != nil { - panic(err) + log.Fatal(err) } + var fdLock *os.File 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 { - panic(err) + fdLock = mustParseFd(v, EnvOODTgtsLockFd) + defer fdLock.Close() + flock := unix.Flock_t{ + Type: unix.F_WRLCK, + Whence: io.SeekStart, } - if _, err = fd.Seek(0, os.SEEK_SET); err != nil { - panic(err) + 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 := ioutil.ReadAll(bufio.NewReader(fd)) + tgtsRaw, err := io.ReadAll(bufio.NewReader(fd)) if err != nil { - panic(err) + log.Fatal(err) + } + flock.Type = unix.F_UNLCK + if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLK, &flock); err != nil { + log.Fatal(err) } - unix.Flock(int(fdLock.Fd()), unix.LOCK_UN) OODTgts = make(map[string]struct{}) for _, tgtRaw := range bytes.Split(tgtsRaw, []byte{0}) { t := string(tgtRaw) @@ -198,7 +270,7 @@ func main() { continue } OODTgts[t] = struct{}{} - trace(CDebug, "ood: known to be: %s", t) + tracef(CDebug, "ood: known to be: %s", t) } } @@ -218,150 +290,314 @@ func main() { fdDep = mustParseFd(v, EnvDepFd) } - tgts := flag.Args() - BuildUUID = os.Getenv(EnvBuildUUID) - if BuildUUID == "" { - raw := new([16]byte) - if _, err = io.ReadFull(rand.Reader, raw[:]); err != nil { - 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 len(tgts) == 0 { - tgts = []string{"all"} + 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) } } - statusInit() + if cmdName == CmdNameRedo { + statusInit() + } - for i, tgt := range tgts { - if path.IsAbs(tgt) { - tgts[i] = cwdMustRel(tgt) + 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 - cmdName := path.Base(os.Args[0]) - trace( - CDebug, "[%s] run: %s %s cwd:%s dirprefix:%s", - BuildUUID, cmdName, tgts, Cwd, DirPrefix, - ) + tracef(CDebug, "[%s] run: %s %s cwd:%s dirprefix:%s", + BuildUUID, cmdName, tgts, Cwd, DirPrefix) -CmdSwitch: switch cmdName { - case "redo": + case CmdNameRedo: for _, tgt := range tgts { - ok, err = ifchange([]string{tgt}, true, traced) + ok, err = ifchange([]*Tgt{tgt}, true, traced) if err != nil || !ok { break } } - case "redo-ifchange": - ok, err = ifchange(tgts, false, traced) + case CmdNameRedoIfchange: + ok, err = ifchange(tgts, *flagForcedIfchange, traced) if err == nil { - err = writeDeps(fdDep, tgts) + err = depsWrite(fdDep, tgts) } - case "redo-ifcreate": + case CmdNameRedoIfcreate: if fdDep == nil { log.Fatalln("no", EnvDepFd) } + fdDepW := bufio.NewWriter(fdDep) for _, tgt := range tgts { - err = ifcreate(fdDep, tgt) + err = ifcreate(fdDepW, fdDep.Name(), tgt.RelTo(path.Join(Cwd, DirPrefix))) if err != nil { break } } - case "redo-always": + err = fdDepW.Flush() + case CmdNameRedoAlways: if fdDep == nil { log.Fatalln("no", EnvDepFd) } - err = always(fdDep) - case "redo-cleanup": + err = always(fdDep, fdDep.Name()) + case CmdNameRedoCleanup: for _, what := range tgts { - err = cleanupWalker(Cwd, what) + err = cleanupWalker(Cwd, path.Base(what.a)) if err != nil { break } } - case "redo-dot": + case CmdNameRedoDot: err = dotPrint(tgts) - case "redo-stamp": + case CmdNameRedoStamp: if fdDep == nil { log.Fatalln("no", EnvDepFd) } - err = stamp(fdDep, os.Stdin) - case "redo-log": + 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.Fatalln("single target expected") + 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} } - d, t := cwdAndTgt(tgts[0]) - var fd *os.File - fd, err = os.Open(path.Join(d, RedoDir, t+LogSuffix)) + raws, err = targetsWalker(raws) if err != nil { + err = ErrLine(err) break } - _, err = io.Copy(os.Stdout, fd) - case "redo-whichdo": - if len(tgts) != 1 { - log.Fatalln("single target expected") + 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)) } - var fdTmp *os.File - fdTmp, err = ioutil.TempFile("", "whichdo") if err != nil { break } - if err = os.Remove(fdTmp.Name()); err != nil { + 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 } - cwd, tgt := cwdAndTgt(tgts[0]) - doFile, upLevels, err := findDo(fdTmp, cwd, tgt) + var build uuid.UUID + build, data, err = depHeadParse(data) if err != nil { break } - _, err = fdTmp.Seek(0, os.SEEK_SET) + w := bufio.NewWriter(os.Stdout) + err = recfileWrite(w, []recfile.Field{ + {Name: "Build", Value: build.String()}, + }...) if err != nil { break } - r := recfile.NewReader(fdTmp) - for { - m, err := r.NextMap() - if err != nil { - if err == io.EOF { - 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}, } - break CmdSwitch - } - 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, "..") + 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)}, + }...) } - p = append(p, doFile) - rel, err := filepath.Rel(Cwd, path.Join(p...)) if err != nil { - panic(err) + break } - fmt.Println(rel) } + err = w.Flush() default: log.Fatalln("unknown command", cmdName) } if err != nil { - log.Println(err) + log.Print(err) } rc := 0 if !ok || err != nil { rc = 1 } - trace(CDebug, "[%s] finished: %s %s", BuildUUID, cmdName, tgts) + tracef(CDebug, "[%s] finished: %s %s", BuildUUID, cmdName, tgts) os.Exit(rc) }