]> Cypherpunks.ru repositories - goredo.git/blob - main.go
Do not forget to install redo-inode
[goredo.git] / main.go
1 /*
2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package main
19
20 import (
21         "bufio"
22         "bytes"
23         "crypto/rand"
24         "errors"
25         "flag"
26         "fmt"
27         "io"
28         "log"
29         "os"
30         "os/signal"
31         "path"
32         "path/filepath"
33         "runtime"
34         "sort"
35         "strconv"
36         "syscall"
37
38         "go.cypherpunks.ru/recfile"
39         "golang.org/x/sys/unix"
40 )
41
42 const (
43         CmdNameGoredo       = "goredo"
44         CmdNameRedo         = "redo"
45         CmdNameRedoAffects  = "redo-affects"
46         CmdNameRedoAlways   = "redo-always"
47         CmdNameRedoCleanup  = "redo-cleanup"
48         CmdNameRedoDot      = "redo-dot"
49         CmdNameRedoIfchange = "redo-ifchange"
50         CmdNameRedoIfcreate = "redo-ifcreate"
51         CmdNameRedoLog      = "redo-log"
52         CmdNameRedoOOD      = "redo-ood"
53         CmdNameRedoSources  = "redo-sources"
54         CmdNameRedoStamp    = "redo-stamp"
55         CmdNameRedoTargets  = "redo-targets"
56         CmdNameRedoWhichdo  = "redo-whichdo"
57         CmdNameRedoDepFix   = "redo-depfix"
58         CmdNameRedoInode    = "redo-inode"
59 )
60
61 var (
62         Cwd       string
63         BuildUUID string
64         IsTopRedo bool // is it the top redo instance
65         UmaskCur  int
66 )
67
68 func mustSetenv(key string) {
69         if err := os.Setenv(key, "1"); err != nil {
70                 panic(err)
71         }
72 }
73
74 func mustParseFd(v, name string) *os.File {
75         ptr, err := strconv.ParseUint(v, 10, 64)
76         if err != nil {
77                 panic(err)
78         }
79         fd := os.NewFile(uintptr(ptr), name)
80         if fd == nil {
81                 panic("can not parse fd: " + name)
82         }
83         return fd
84 }
85
86 func CmdName() string {
87         return path.Base(os.Args[0])
88 }
89
90 func main() {
91         version := flag.Bool("version", false, "print version")
92         warranty := flag.Bool("warranty", false, "print warranty information")
93
94         var symlinks *bool
95         cmdName := CmdName()
96         if cmdName == "goredo" {
97                 symlinks = flag.Bool("symlinks", false, "create necessary symlinks in current directory")
98         }
99
100         flag.Usage = func() { usage(os.Args[0]) }
101         flag.Parse()
102         if *warranty {
103                 fmt.Println(Warranty)
104                 return
105         }
106         if *version {
107                 fmt.Println("goredo", Version, "built with", runtime.Version())
108                 return
109         }
110         if cmdName == CmdNameGoredo && *symlinks {
111                 rc := 0
112                 for _, cmdName := range []string{
113                         CmdNameRedo,
114                         CmdNameRedoAffects,
115                         CmdNameRedoAlways,
116                         CmdNameRedoCleanup,
117                         CmdNameRedoDepFix,
118                         CmdNameRedoDot,
119                         CmdNameRedoIfchange,
120                         CmdNameRedoIfcreate,
121                         CmdNameRedoInode,
122                         CmdNameRedoLog,
123                         CmdNameRedoOOD,
124                         CmdNameRedoSources,
125                         CmdNameRedoStamp,
126                         CmdNameRedoTargets,
127                         CmdNameRedoWhichdo,
128                 } {
129                         fmt.Println(os.Args[0], "<-", cmdName)
130                         if err := os.Symlink(os.Args[0], cmdName); err != nil {
131                                 rc = 1
132                                 log.Print(err)
133                         }
134                 }
135                 os.Exit(rc)
136         }
137         log.SetFlags(log.Lshortfile)
138
139         UmaskCur = syscall.Umask(0)
140         syscall.Umask(UmaskCur)
141
142         var err error
143         Cwd, err = os.Getwd()
144         if err != nil {
145                 log.Fatal(err)
146         }
147
148         TopDir = os.Getenv(EnvTopDir)
149         if TopDir == "" {
150                 TopDir = "/"
151         } else {
152                 TopDir = mustAbs(TopDir)
153         }
154         DirPrefix = os.Getenv(EnvDirPrefix)
155         DepCwd = os.Getenv(EnvDepCwd)
156
157         if flagStderrKeep != nil && *flagStderrKeep {
158                 mustSetenv(EnvStderrKeep)
159         }
160         if flagStderrSilent != nil && *flagStderrSilent {
161                 mustSetenv(EnvStderrSilent)
162         }
163         if flagNoProgress != nil && *flagNoProgress {
164                 mustSetenv(EnvNoProgress)
165         }
166         if flagDebug != nil && *flagDebug {
167                 mustSetenv(EnvDebug)
168         }
169         if flagLogWait != nil && *flagLogWait {
170                 mustSetenv(EnvLogWait)
171         }
172         if flagLogLock != nil && *flagLogLock {
173                 mustSetenv(EnvLogLock)
174         }
175         if flagLogPid != nil && *flagLogPid {
176                 mustSetenv(EnvLogPid)
177         }
178         if flagLogJS != nil && *flagLogJS {
179                 mustSetenv(EnvLogJS)
180         }
181         StderrKeep = os.Getenv(EnvStderrKeep) == "1"
182         StderrSilent = os.Getenv(EnvStderrSilent) == "1"
183         NoProgress = os.Getenv(EnvNoProgress) == "1"
184         Debug = os.Getenv(EnvDebug) == "1"
185         LogWait = os.Getenv(EnvLogWait) == "1"
186         LogLock = os.Getenv(EnvLogLock) == "1"
187         LogJS = os.Getenv(EnvLogJS) == "1"
188         if Debug || os.Getenv(EnvLogPid) == "1" {
189                 MyPid = os.Getpid()
190         }
191         var traced bool
192         if flagTraceAll != nil && *flagTraceAll {
193                 mustSetenv(EnvTrace)
194         }
195         if os.Getenv(EnvTrace) == "1" {
196                 TracedAll = true
197                 traced = true
198         } else if flagTrace != nil {
199                 traced = *flagTrace
200         }
201         NoColor = os.Getenv(EnvNoColor) != ""
202         NoSync = os.Getenv(EnvNoSync) == "1"
203         StopIfMod = os.Getenv(EnvStopIfMod) == "1"
204         switch s := os.Getenv(EnvInodeTrust); s {
205         case "none":
206                 InodeTrust = InodeTrustNone
207         case "", "ctime":
208                 InodeTrust = InodeTrustCtime
209         case "mtime":
210                 InodeTrust = InodeTrustMtime
211         default:
212                 log.Fatalln("unknown", EnvInodeTrust, "value")
213         }
214         tracef(CDebug, "inode-trust: %s", InodeTrust)
215
216         // Those are internal envs
217         FdOODTgts, err = os.CreateTemp("", "ood-tgts")
218         if err != nil {
219                 log.Fatal(err)
220         }
221         if err = os.Remove(FdOODTgts.Name()); err != nil {
222                 log.Fatal(err)
223         }
224         FdOODTgtsLock, err = os.CreateTemp("", "ood-tgts.lock")
225         if err != nil {
226                 log.Fatal(err)
227         }
228         if err = os.Remove(FdOODTgtsLock.Name()); err != nil {
229                 log.Fatal(err)
230         }
231
232         var fdLock *os.File
233         if v := os.Getenv(EnvOODTgtsFd); v != "" {
234                 fd := mustParseFd(v, EnvOODTgtsFd)
235                 fdLock = mustParseFd(v, EnvOODTgtsLockFd)
236                 defer fdLock.Close()
237                 flock := unix.Flock_t{
238                         Type:   unix.F_WRLCK,
239                         Whence: io.SeekStart,
240                 }
241                 if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLKW, &flock); err != nil {
242                         log.Fatal(err)
243                 }
244                 if _, err = fd.Seek(0, io.SeekStart); err != nil {
245                         log.Fatal(err)
246                 }
247                 tgtsRaw, err := io.ReadAll(bufio.NewReader(fd))
248                 if err != nil {
249                         log.Fatal(err)
250                 }
251                 flock.Type = unix.F_UNLCK
252                 if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLK, &flock); err != nil {
253                         log.Fatal(err)
254                 }
255                 OODTgts = map[string]struct{}{}
256                 for _, tgtRaw := range bytes.Split(tgtsRaw, []byte{0}) {
257                         t := string(tgtRaw)
258                         if t == "" {
259                                 continue
260                         }
261                         OODTgts[t] = struct{}{}
262                         tracef(CDebug, "ood: known to be: %s", t)
263                 }
264         }
265
266         StderrPrefix = os.Getenv(EnvStderrPrefix)
267         if v := os.Getenv(EnvLevel); v != "" {
268                 Level, err = strconv.Atoi(v)
269                 if err != nil {
270                         panic(err)
271                 }
272                 if Level < 0 {
273                         panic("negative " + EnvLevel)
274                 }
275         }
276
277         var fdDep *os.File
278         if v := os.Getenv(EnvDepFd); v != "" {
279                 fdDep = mustParseFd(v, EnvDepFd)
280         }
281
282         tgts := flag.Args()
283         BuildUUID = os.Getenv(EnvBuildUUID)
284         tgtsWasEmpty := len(tgts) == 0
285         if BuildUUID == "" {
286                 IsTopRedo = true
287                 raw := new([16]byte)
288                 if _, err = io.ReadFull(rand.Reader, raw[:]); err != nil {
289                         log.Fatal(err)
290                 }
291                 raw[6] = (raw[6] & 0x0F) | uint8(4<<4) // version 4
292                 BuildUUID = fmt.Sprintf(
293                         "%x-%x-%x-%x-%x",
294                         raw[0:4], raw[4:6], raw[6:8], raw[8:10], raw[10:],
295                 )
296                 if tgtsWasEmpty {
297                         tgts = []string{"all"}
298                 }
299         }
300
301         if cmdName == CmdNameRedo {
302                 statusInit()
303         }
304
305         for i, tgt := range tgts {
306                 if path.IsAbs(tgt) {
307                         tgts[i] = cwdMustRel(tgt)
308                 }
309         }
310
311         killed := make(chan os.Signal, 1)
312         signal.Notify(killed, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
313         go func() {
314                 <-killed
315                 tracef(CDebug, "[%s] killed", BuildUUID)
316                 jsReleaseAll()
317                 RunningProcsM.Lock()
318                 for pid, proc := range RunningProcs {
319                         tracef(CDebug, "[%s] killing child %d", BuildUUID, pid)
320                         _ = proc.Signal(syscall.SIGTERM)
321                 }
322                 os.Exit(1)
323         }()
324
325         ok := true
326         err = nil
327         tracef(
328                 CDebug, "[%s] run: %s %s cwd:%s dirprefix:%s",
329                 BuildUUID, cmdName, tgts, Cwd, DirPrefix,
330         )
331
332 CmdSwitch:
333         switch cmdName {
334         case CmdNameRedo:
335                 for _, tgt := range tgts {
336                         ok, err = ifchange([]string{tgt}, true, traced)
337                         if err != nil || !ok {
338                                 break
339                         }
340                 }
341         case CmdNameRedoIfchange:
342                 ok, err = ifchange(tgts, *flagForcedIfchange, traced)
343                 if err == nil {
344                         err = depsWrite(fdDep, tgts)
345                 }
346         case CmdNameRedoIfcreate:
347                 if fdDep == nil {
348                         log.Fatalln("no", EnvDepFd)
349                 }
350                 for _, tgt := range tgts {
351                         tgtRel, err := filepath.Rel(
352                                 path.Join(Cwd, DirPrefix),
353                                 path.Join(Cwd, tgt),
354                         )
355                         if err != nil {
356                                 err = ErrLine(err)
357                                 break
358                         }
359                         err = ifcreate(fdDep, tgtRel)
360                         if err != nil {
361                                 break
362                         }
363                 }
364         case CmdNameRedoAlways:
365                 if fdDep == nil {
366                         log.Fatalln("no", EnvDepFd)
367                 }
368                 err = always(fdDep)
369         case CmdNameRedoCleanup:
370                 for _, what := range tgts {
371                         err = cleanupWalker(Cwd, what)
372                         if err != nil {
373                                 break
374                         }
375                 }
376         case CmdNameRedoDot:
377                 err = dotPrint(tgts)
378         case CmdNameRedoStamp:
379                 if fdDep == nil {
380                         log.Fatalln("no", EnvDepFd)
381                 }
382                 err = stamp(fdDep, os.Stdin)
383         case CmdNameRedoLog:
384                 if len(tgts) != 1 {
385                         log.Fatal("single target expected")
386                 }
387                 d, t := cwdAndTgt(tgts[0])
388                 err = showBuildLog(d, t, nil, 0)
389         case CmdNameRedoWhichdo:
390                 if len(tgts) != 1 {
391                         log.Fatal("single target expected")
392                 }
393                 var fdTmp *os.File
394                 fdTmp, err = os.CreateTemp("", "whichdo")
395                 if err != nil {
396                         err = ErrLine(err)
397                         break
398                 }
399                 if err = os.Remove(fdTmp.Name()); err != nil {
400                         err = ErrLine(err)
401                         break
402                 }
403                 cwd, tgt := cwdAndTgt(tgts[0])
404                 doFile, upLevels, err := findDo(fdTmp, cwd, tgt)
405                 if err != nil {
406                         err = ErrLine(err)
407                         break
408                 }
409                 _, err = fdTmp.Seek(0, io.SeekStart)
410                 if err != nil {
411                         err = ErrLine(err)
412                         break
413                 }
414                 r := recfile.NewReader(fdTmp)
415                 for {
416                         m, err := r.NextMap()
417                         if err != nil {
418                                 if errors.Is(err, io.EOF) {
419                                         break
420                                 }
421                                 err = ErrLine(err)
422                                 break CmdSwitch
423                         }
424                         fmt.Println(cwdMustRel(cwd, m["Target"]))
425                 }
426                 if doFile == "" {
427                         ok = false
428                 } else {
429                         p := make([]string, 0, upLevels+2)
430                         p = append(p, cwd)
431                         for i := 0; i < upLevels; i++ {
432                                 p = append(p, "..")
433                         }
434                         p = append(p, doFile)
435                         rel := mustRel(Cwd, path.Join(p...))
436                         fmt.Println(rel)
437                 }
438         case CmdNameRedoTargets:
439                 if tgtsWasEmpty {
440                         tgts = []string{Cwd}
441                 }
442                 tgts, err = targetsWalker(tgts)
443                 if err != nil {
444                         err = ErrLine(err)
445                         break
446                 }
447                 sort.Strings(tgts)
448                 for _, tgt := range tgts {
449                         fmt.Println(tgt)
450                 }
451         case CmdNameRedoAffects:
452                 if tgtsWasEmpty {
453                         log.Fatal("no targets specified")
454                 }
455                 var tgtsKnown []string
456                 tgtsKnown, err = targetsWalker([]string{Cwd})
457                 if err != nil {
458                         err = ErrLine(err)
459                         break
460                 }
461                 deps := map[string]map[string]struct{}{}
462                 for _, tgt := range tgtsKnown {
463                         collectDeps(Cwd, tgt, 0, deps, true, map[string]struct{}{})
464                 }
465                 seen := map[string]struct{}{}
466                 for _, tgt := range tgts {
467                         collectWholeDeps(deps[tgt], deps, seen)
468                 }
469                 tgts := make([]string, 0, len(seen))
470                 for dep := range seen {
471                         tgts = append(tgts, dep)
472                 }
473                 sort.Strings(tgts)
474                 for _, dep := range tgts {
475                         fmt.Println(dep)
476                 }
477         case CmdNameRedoOOD:
478                 if tgtsWasEmpty {
479                         tgts, err = targetsWalker([]string{Cwd})
480                         if err != nil {
481                                 break
482                         }
483                 }
484                 sort.Strings(tgts)
485                 var ood bool
486                 for _, tgt := range tgts {
487                         ood, err = isOOD(Cwd, tgt, 0, nil)
488                         if err != nil {
489                                 err = ErrLine(err)
490                                 break
491                         }
492                         if ood {
493                                 fmt.Println(tgt)
494                         }
495                 }
496         case CmdNameRedoSources:
497                 if tgtsWasEmpty {
498                         tgts, err = targetsWalker([]string{Cwd})
499                         if err != nil {
500                                 err = ErrLine(err)
501                                 break
502                         }
503                 }
504                 sort.Strings(tgts)
505                 seen := make(map[string]struct{})
506                 seenDeps := make(map[string]struct{})
507                 srcs := make(map[string]struct{})
508                 err = ErrLine(sourcesWalker(tgts, seen, seenDeps, srcs))
509                 seen = nil
510                 seenDeps = nil
511                 if err != nil {
512                         break
513                 }
514                 seenDeps = nil
515                 res := make([]string, 0, len(srcs))
516                 for p := range srcs {
517                         res = append(res, p)
518                 }
519                 srcs = nil
520                 sort.Strings(res)
521                 for _, src := range res {
522                         fmt.Println(src)
523                 }
524         case CmdNameRedoDepFix:
525                 err = depFix(Cwd)
526         case CmdNameRedoInode:
527                 var inode *Inode
528                 for _, tgt := range tgts {
529                         inode, err = inodeFromFileByPath(tgt)
530                         if err != nil {
531                                 err = ErrLine(err)
532                                 break
533                         }
534                         err = recfileWrite(os.Stdout, append(
535                                 []recfile.Field{{Name: "Target", Value: tgt}},
536                                 inode.RecfileFields()...)...)
537                         if err != nil {
538                                 err = ErrLine(err)
539                                 break
540                         }
541                 }
542         default:
543                 log.Fatalln("unknown command", cmdName)
544         }
545         if err != nil {
546                 log.Print(err)
547         }
548         rc := 0
549         if !ok || err != nil {
550                 rc = 1
551         }
552         tracef(CDebug, "[%s] finished: %s %s", BuildUUID, cmdName, tgts)
553         os.Exit(rc)
554 }