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