/* goredo -- redo implementation on pure Go Copyright (C) 2020 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 . */ // Out-of-date determination package main import ( "errors" "fmt" "io" "os" "path" "path/filepath" "strings" "go.cypherpunks.ru/recfile" ) const ( DepTypeIfcreate = "ifcreate" DepTypeIfchange = "ifchange" DepTypeAlways = "always" DepTypeStamp = "stamp" ) type TgtErr struct { Tgt string Err error } func (e TgtErr) Unwrap() error { return e.Err } func (e TgtErr) Error() string { return fmt.Sprintf("%s: %s", e.Tgt, e.Err) } func cwdMustRel(paths ...string) string { rel, err := filepath.Rel(Cwd, path.Join(paths...)) if err != nil { panic(err) } return rel } func cwdAndTgt(tgt string) (string, string) { cwd, tgt := path.Split(tgt) cwd, err := filepath.Abs(cwd) if err != nil { panic(err) } return cwd, tgt } func isSrc(cwd, tgt string) bool { d, f := path.Split(path.Join(cwd, tgt)) if _, err := os.Stat(path.Join(d, f)); err != nil { return false } if _, err := os.Stat(path.Join(d, RedoDir, f+DepSuffix)); err == nil { return false } return true } type DepInfo struct { build string always bool stamp string stampSame bool ifcreates []string ifchanges []map[string]string } func depRead(fdDep *os.File) (*DepInfo, error) { r := recfile.NewReader(fdDep) m, err := r.NextMap() if err != nil { return nil, err } depInfo := DepInfo{} if b := m["Build"]; b == "" { return nil, errors.New(".dep missing Build:") } else { depInfo.build = b } for { m, err := r.NextMap() if err != nil { if err == io.EOF { break } return nil, err } switch m["Type"] { case DepTypeAlways: depInfo.always = true case DepTypeIfcreate: dep := m["Target"] if dep == "" { return nil, errors.New("invalid format of .dep") } depInfo.ifcreates = append(depInfo.ifcreates, dep) case DepTypeIfchange: delete(m, "Type") depInfo.ifchanges = append(depInfo.ifchanges, m) case DepTypeStamp: hsh := m["Hash"] if hsh == "" { return nil, errors.New("invalid format of .dep") } depInfo.stamp = hsh depInfo.stampSame = m["Same"] == "true" default: return nil, errors.New("invalid format of .dep") } } return &depInfo, nil } func rebuildStamped(cwd, tgt, depPath, stampPrev string) (bool, error) { relTgt := cwdMustRel(cwd, tgt) errs := make(chan error, 1) if err := runScript(relTgt, errs, stampPrev); err != nil { return false, err } if err := <-errs; !isOkRun(err) { return false, errors.New("build failed") } fdDep, err := os.Open(depPath) if err != nil { return false, err } defer fdDep.Close() depInfo, err := depRead(fdDep) if err != nil { return false, err } if depInfo.build != BuildUUID { return false, errors.New("is not built") } return depInfo.stampSame, nil } func formDepPath(cwd, tgt string) string { cwd, tgt = cwdAndTgt(path.Join(cwd, tgt)) return path.Join(cwd, RedoDir, tgt+DepSuffix) } func isOOD(cwd, tgtOrig string, level int, depInfo *DepInfo) (bool, error) { indent := strings.Repeat(". ", level) trace(CDebug, "ood: %s%s checking", indent, tgtOrig) cwd, tgt := cwdAndTgt(path.Join(cwd, tgtOrig)) depPath := formDepPath(cwd, tgt) if depInfo == nil { fdDep, err := os.Open(depPath) if err != nil { trace(CDebug, "ood: %s%s -> no dep: %s", indent, tgtOrig, depPath) return true, nil } defer fdDep.Close() depInfo, err = depRead(fdDep) if err != nil { return true, TgtErr{tgtOrig, err} } } if depInfo.build == BuildUUID { trace(CDebug, "ood: %s%s -> already built", indent, tgtOrig) return false, nil } ood := depInfo.always if ood { goto StampCheck } for _, dep := range depInfo.ifcreates { if _, err := os.Stat(path.Join(cwd, dep)); err == nil { trace(CDebug, "ood: %s%s -> %s created", indent, tgtOrig, dep) ood = true goto StampCheck } } for _, m := range depInfo.ifchanges { dep := m["Target"] theirTs := m["Ctime"] theirHsh := m["Hash"] if dep == "" || theirTs == "" { return ood, TgtErr{tgtOrig, errors.New("invalid format of .dep")} } trace(CDebug, "ood: %s%s -> %s: checking", indent, tgtOrig, dep) fd, err := os.Open(path.Join(cwd, dep)) if err != nil { if os.IsNotExist(err) { trace(CDebug, "ood: %s%s -> %s: not exists", indent, tgtOrig, dep) ood = true goto StampCheck } return ood, TgtErr{tgtOrig, err} } defer fd.Close() var depDepInfo *DepInfo if !(dep == tgt || isSrc(cwd, dep)) { trace(CDebug, "ood: %s%s -> %s: prereading .dep", indent, tgtOrig, dep) depFdDep, err := os.Open(formDepPath(cwd, dep)) if err != nil { return ood, TgtErr{path.Join(tgtOrig, dep), err} } defer depFdDep.Close() depDepInfo, err = depRead(depFdDep) if err != nil { return ood, TgtErr{path.Join(tgtOrig, dep), err} } } if depDepInfo != nil && depDepInfo.build == BuildUUID { trace( CDebug, "ood: %s%s -> %s: .dep says build is same", indent, tgtOrig, dep, ) if !depDepInfo.stampSame { trace( CDebug, "ood: %s%s -> %s: .dep says stamp is not same", indent, tgtOrig, dep, ) ood = true return ood, nil } trace( CDebug, "ood: %s%s -> %s: .dep says stamp is same", indent, tgtOrig, dep, ) continue } if depDepInfo == nil || !depDepInfo.always && depDepInfo.stamp == "" { ts, err := fileCtime(fd) if err != nil { return ood, TgtErr{tgtOrig, err} } if theirTs == ts { trace(CDebug, "ood: %s%s -> %s: same ctime", indent, tgtOrig, dep) } else if NoHash || theirHsh == "" { trace(CDebug, "ood: %s%s -> %s: ctime differs", indent, tgtOrig, dep) ood = true goto StampCheck } else { hsh, err := fileHash(fd) if err != nil { return ood, TgtErr{tgtOrig, err} } if theirHsh != hsh { trace(CDebug, "ood: %s%s -> %s: hash differs", indent, tgtOrig, dep) ood = true goto StampCheck } trace(CDebug, "ood: %s%s -> %s: same hash", indent, tgtOrig, dep) } } if dep == tgt { trace(CDebug, "ood: %s%s -> %s: same target", indent, tgtOrig, dep) continue } if isSrc(cwd, dep) { trace(CDebug, "ood: %s%s -> %s: is source", indent, tgtOrig, dep) continue } depOod, err := isOOD(cwd, dep, level+1, depDepInfo) if err != nil { return ood, TgtErr{tgtOrig, err} } if depOod { trace(CDebug, "ood: %s%s -> %s: ood", indent, tgtOrig, dep) ood = true goto StampCheck } trace(CDebug, "ood: %s%s -> %s: !ood", indent, tgtOrig, dep) } StampCheck: if ood && depInfo.stamp != "" { trace(CDebug, "ood: %s%s run, because stamped", indent, tgtOrig) stampSame, err := rebuildStamped(cwd, tgt, depPath, depInfo.stamp) if err != nil { return true, TgtErr{tgtOrig, err} } trace(CDebug, "ood: %s%s -> stamp: same: %v", indent, tgtOrig, stampSame) ood = !stampSame } trace(CDebug, "ood: %s%s: %v", indent, tgtOrig, ood) return ood, nil }