/*
-goredo -- redo implementation on pure Go
-Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+goredo -- djb's redo implementation on pure Go
+Copyright (C) 2020-2023 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
package main
import (
- "crypto/rand"
+ "bufio"
+ "bytes"
+ "encoding/hex"
"flag"
"fmt"
"io"
- "io/ioutil"
"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)
}
}
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 == "goredo" {
+ 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 {
- panic(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"
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 = 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)
panic("negative " + EnvLevel)
}
}
+
var fdDep *os.File
if v := os.Getenv(EnvDepFd); v != "" {
fdDep = mustParseFd(v, EnvDepFd)
}
- BuildUUID = os.Getenv(EnvBuildUUID)
- tgts := flag.Args()
- if BuildUUID == "" {
- raw := new([16]byte)
- if _, err = io.ReadFull(rand.Reader, raw[:]); err != nil {
- panic(err)
+
+ 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"))
}
- 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"}
+ tracef(CDebug, "inode-trust: %s", InodeTrust)
+ } else {
+ BuildUUID, err = uuid.Parse(BuildUUIDStr)
+ if err != nil {
+ log.Fatal(err)
}
}
- statusInit()
- for i, tgt := range tgts {
- if path.IsAbs(tgt) {
- tgts[i] = cwdMustRel(tgt)
- }
+ 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
- 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":
- 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))
+ var hsh Hash
+ hsh, err = fileHash(os.Stdin)
if err != nil {
break
}
- _, err = io.Copy(os.Stdout, fd)
- case "redo-whichdo":
+ err = stamp(fdDep, fdDep.Name(), hsh)
+ case CmdNameRedoLog:
if len(tgts) != 1 {
- log.Fatalln("single target expected")
+ log.Fatal("single target expected")
}
- var fdTmp *os.File
- fdTmp, err = ioutil.TempFile("", "")
- if err != nil {
- break
+ err = showBuildLog(tgts[0], nil, 0)
+ case CmdNameRedoWhichdo:
+ if len(tgts) != 1 {
+ log.Fatal("single target expected")
}
- os.Remove(fdTmp.Name())
- cwd, tgt := cwdAndTgt(tgts[0])
- doFile, upLevels, err := findDo(fdTmp, cwd, tgt)
+ var dos []string
+ dos, err = whichdo(tgts[0])
if err != nil {
break
}
- _, err = fdTmp.Seek(0, 0)
+ sort.Strings(dos)
+ 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
}
- r := recfile.NewReader(fdTmp)
- for {
- m, err := r.NextMap()
+ 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 {
- if err == io.EOF {
+ 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
}
- 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, "..")
+ 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
+ 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 := string(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(),
+ })
+ fields = append(fields, Inode(meta[:InodeLen]).RecfileFields()...)
+ err = recfileWrite(w, fields...)
+ case DepTypeIfchangeDummy:
+ 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)
}