From 67e92d5a298516292bbbee1f7714708cdc0766f8 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sat, 21 Nov 2020 20:09:33 +0300 Subject: [PATCH] DOT generation --- .gitignore | 2 ++ README | 5 +++ cleanup.go | 11 ++----- dep.go | 11 +++---- dot.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 9 +++--- ood.go | 39 ++++++++++++++-------- run.go | 5 ++- usage.go | 4 ++- 9 files changed, 142 insertions(+), 38 deletions(-) create mode 100644 dot.go diff --git a/.gitignore b/.gitignore index 2156cc9..40fa2fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ +/goredo /redo /redo-always /redo-cleanup +/redo-dot /redo-ifchange /redo-ifcreate /redo-log diff --git a/README b/README index a918bae..022ae20 100644 --- a/README +++ b/README @@ -83,6 +83,11 @@ COMMANDS *goredo-commands* default.do ../default.b.o.do ../default.o.do +* redo-dot -- dependency DOT graph generator. For example to visualize + your dependencies with GraphViz: > + $ redo target [...] # to assure that .redo is filled up + $ redo-dot target [...] > whatever.dot + $ dot -Tpng whatever.dot > whatever.png # possibly add -Gsplines=ortho < STATE *goredo-state* diff --git a/cleanup.go b/cleanup.go index a98510d..bb9564c 100644 --- a/cleanup.go +++ b/cleanup.go @@ -22,7 +22,6 @@ import ( "io" "log" "os" - "path" "path/filepath" "strings" ) @@ -52,10 +51,7 @@ func redoDirClean(root, what string) error { } var pth string for _, fi := range fis { - pth, err = filepath.Rel(Cwd, path.Join(root, fi.Name())) - if err != nil { - panic(err) - } + pth = cwdMustRel(root, fi.Name()) switch what { case CleanupLog: if strings.HasSuffix(fi.Name(), LogSuffix) { @@ -99,10 +95,7 @@ func cleanupWalker(root, what string) error { } var pth string for _, fi := range fis { - pth, err = filepath.Rel(Cwd, path.Join(root, fi.Name())) - if err != nil { - panic(err) - } + pth = cwdMustRel(root, fi.Name()) if fi.IsDir() { if fi.Name() == RedoDir { if what == CleanupFull { diff --git a/dep.go b/dep.go index 6900357..0f62d30 100644 --- a/dep.go +++ b/dep.go @@ -52,14 +52,14 @@ func ifcreate(fdDep *os.File, tgt string) error { trace(CDebug, "ifcreate: %s <- %s", fdDep.Name(), tgt) return recfileWrite( fdDep, - recfile.Field{Name: "Type", Value: "ifcreate"}, + recfile.Field{Name: "Type", Value: DepTypeIfcreate}, recfile.Field{Name: "Target", Value: tgt}, ) } func always(fdDep *os.File) error { trace(CDebug, "always: %s", fdDep.Name()) - return recfileWrite(fdDep, recfile.Field{Name: "Type", Value: "always"}) + return recfileWrite(fdDep, recfile.Field{Name: "Type", Value: DepTypeAlways}) } func stamp(fdDep, src *os.File) error { @@ -71,7 +71,7 @@ func stamp(fdDep, src *os.File) error { trace(CDebug, "stamp: %s <- %s", fdDep.Name(), hsh) return recfileWrite( fdDep, - recfile.Field{Name: "Type", Value: "stamp"}, + recfile.Field{Name: "Type", Value: DepTypeStamp}, recfile.Field{Name: "Hash", Value: hsh}, ) } @@ -109,7 +109,7 @@ func writeDep(fdDep *os.File, cwd, tgt string) error { return err } fields := []recfile.Field{ - recfile.Field{Name: "Type", Value: "ifchange"}, + recfile.Field{Name: "Type", Value: DepTypeIfchange}, recfile.Field{Name: "Target", Value: tgt}, recfile.Field{Name: "Ctime", Value: ts}, } @@ -121,8 +121,7 @@ func writeDep(fdDep *os.File, cwd, tgt string) error { } fields = append(fields, recfile.Field{Name: "Hash", Value: hsh}) } - err = recfileWrite(fdDep, fields...) - return err + return recfileWrite(fdDep, fields...) } func writeDeps(fdDep *os.File, tgts []string) error { diff --git a/dot.go b/dot.go new file mode 100644 index 0000000..d59bc40 --- /dev/null +++ b/dot.go @@ -0,0 +1,94 @@ +/* +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 . +*/ + +// Dependency DOT graph generation + +package main + +import ( + "fmt" + "io" + "os" + "path" + + "go.cypherpunks.ru/recfile" +) + +type DotNodes struct { + from string + to string +} + +func dotWalker(data map[DotNodes]bool, tgtOrig string) (map[DotNodes]bool, error) { + cwd, tgt := cwdAndTgt(tgtOrig) + depPath := path.Join(cwd, RedoDir, tgt+DepSuffix) + fdDep, err := os.Open(depPath) + if err != nil { + return nil, err + } + defer fdDep.Close() + var dep string + r := recfile.NewReader(fdDep) + for { + m, err := r.NextMap() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + switch m["Type"] { + case DepTypeIfcreate: + data[DotNodes{tgtOrig, cwdMustRel(cwd, m["Target"])}] = true + case DepTypeIfchange: + dep = m["Target"] + data[DotNodes{tgtOrig, cwdMustRel(cwd, dep)}] = false + if isSrc(cwd, dep) || dep == tgt { + continue + } + data, err = dotWalker(data, cwdMustRel(cwd, dep)) + if err != nil { + return nil, err + } + } + } + return data, nil +} + +func dotPrint(tgts []string) error { + data := map[DotNodes]bool{} + var err error + for _, tgt := range tgts { + data, err = dotWalker(data, tgt) + if err != nil { + return err + } + } + fmt.Println(`digraph d { + rankdir=LR + ranksep=2 + splines=false // splines=ortho + node[shape=rectangle]`) + for nodes, nonexistent := range data { + fmt.Printf("\n\t\"%s\" -> \"%s\"", nodes.from, nodes.to) + if nonexistent { + fmt.Print(" [style=dotted]") + } + } + fmt.Println("\n}") + return nil +} diff --git a/main.go b/main.go index 42d83be..fdd0aac 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,7 @@ func main() { "redo", "redo-always", "redo-cleanup", + "redo-dot", "redo-ifchange", "redo-ifcreate", "redo-log", @@ -213,6 +214,8 @@ CmdSwitch: break } } + case "redo-dot": + err = dotPrint(tgts) case "redo-stamp": if fdDep == nil { log.Fatalln("no", EnvDepFd) @@ -255,11 +258,7 @@ CmdSwitch: } break CmdSwitch } - rel, err := filepath.Rel(Cwd, path.Join(cwd, m["Target"])) - if err != nil { - panic(err) - } - fmt.Println(rel) + fmt.Println(cwdMustRel(cwd, m["Target"])) } ok = doFile != "" default: diff --git a/ood.go b/ood.go index 4189ed4..15d0ae5 100644 --- a/ood.go +++ b/ood.go @@ -31,6 +31,13 @@ import ( "go.cypherpunks.ru/recfile" ) +const ( + DepTypeIfcreate = "ifcreate" + DepTypeIfchange = "ifchange" + DepTypeAlways = "always" + DepTypeStamp = "stamp" +) + type TgtErr struct { Tgt string Err error @@ -42,6 +49,14 @@ 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) @@ -75,15 +90,12 @@ func isBuiltNow(fdDep *os.File) (bool, *recfile.Reader, error) { } func rebuildStamped(cwd, tgt, depPath string) (string, error) { - relTgt, err := filepath.Rel(Cwd, path.Join(cwd, tgt)) - if err != nil { - panic(err) - } + relTgt := cwdMustRel(cwd, tgt) errs := make(chan error, 1) - if err = runScript(relTgt, errs); err != nil { + if err := runScript(relTgt, errs); err != nil { return "", err } - if err = <-errs; !isOkRun(err) { + if err := <-errs; !isOkRun(err) { return "", errors.New("build failed") } fdDep, err := os.Open(depPath) @@ -107,7 +119,7 @@ func rebuildStamped(cwd, tgt, depPath string) (string, error) { } return "", err } - if m["Type"] == "stamp" { + if m["Type"] == DepTypeStamp { stampTheir = m["Hash"] break } @@ -115,11 +127,10 @@ func rebuildStamped(cwd, tgt, depPath string) (string, error) { return stampTheir, nil } -func isOOD(cwd, tgt string, level int) (bool, error) { - tgtOrig := tgt +func isOOD(cwd, tgtOrig string, level int) (bool, error) { indent := strings.Repeat(". ", level) trace(CDebug, "ood: %s%s checking", indent, tgtOrig) - cwd, tgt = cwdAndTgt(path.Join(cwd, tgt)) + cwd, tgt := cwdAndTgt(path.Join(cwd, tgtOrig)) depPath := path.Join(cwd, RedoDir, tgt+DepSuffix) fdDep, err := os.Open(depPath) if err != nil { @@ -150,14 +161,14 @@ func isOOD(cwd, tgt string, level int) (bool, error) { return true, TgtErr{tgtOrig, err} } switch m["Type"] { - case "always": + case DepTypeAlways: trace(CDebug, "ood: %s%s -> always", indent, tgtOrig) ood = true - case "ifcreate": + case DepTypeIfcreate: ifcreates = append(ifcreates, m) - case "ifchange": + case DepTypeIfchange: ifchanges = append(ifchanges, m) - case "stamp": + case DepTypeStamp: stampOur = m["Hash"] trace(CDebug, "ood: %s%s -> stamped: %s", indent, tgtOrig, stampOur) default: diff --git a/run.go b/run.go index fab6737..c73d8b4 100644 --- a/run.go +++ b/run.go @@ -162,9 +162,8 @@ func syncDir(dir string) error { return err } -func runScript(tgt string, errs chan error) error { - tgtOrig := tgt - cwd, tgt := cwdAndTgt(tgt) +func runScript(tgtOrig string, errs chan error) error { + cwd, tgt := cwdAndTgt(tgtOrig) redoDir := path.Join(cwd, RedoDir) if err := mkdirs(redoDir); err != nil { return TgtErr{tgtOrig, err} diff --git a/usage.go b/usage.go index d01d85d..d29e1ca 100644 --- a/usage.go +++ b/usage.go @@ -53,7 +53,7 @@ License GPLv3: GNU GPL version 3 This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. -redo, redo-{always,cleanup,ifchange,ifcreate,log,stamp,whichdo} must be +redo, redo-{always,cleanup,dot,ifchange,ifcreate,log,stamp,whichdo} must be linked to goredo executable. It determines the command by its own name. You can create them by running: goredo -symlinks. @@ -64,6 +64,8 @@ You can create them by running: goredo -symlinks. * redo-cleanup {full,log,tmp} [...] remove either all goredo's related temporary files, or kept stderr logs, or everything (including .redo directories) related +* redo-dot target [...] + write dependency DOT graph to stdout * redo-ifchange target [...] build specified targets in parallel, if they are changed. Record them as dependencies for current target -- 2.44.0