]> Cypherpunks.ru repositories - goredo.git/blob - main.go
018aaf177474a914fd77536712fcc83438312d80
[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
214         // Those are internal envs
215         FdOODTgts, err = os.CreateTemp("", "ood-tgts")
216         if err != nil {
217                 log.Fatal(err)
218         }
219         if err = os.Remove(FdOODTgts.Name()); err != nil {
220                 log.Fatal(err)
221         }
222         FdOODTgtsLock, err = os.CreateTemp("", "ood-tgts.lock")
223         if err != nil {
224                 log.Fatal(err)
225         }
226         if err = os.Remove(FdOODTgtsLock.Name()); err != nil {
227                 log.Fatal(err)
228         }
229
230         var fdLock *os.File
231         if v := os.Getenv(EnvOODTgtsFd); v != "" {
232                 fd := mustParseFd(v, EnvOODTgtsFd)
233                 fdLock = mustParseFd(v, EnvOODTgtsLockFd)
234                 defer fdLock.Close()
235                 flock := unix.Flock_t{
236                         Type:   unix.F_WRLCK,
237                         Whence: io.SeekStart,
238                 }
239                 if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLKW, &flock); err != nil {
240                         log.Fatal(err)
241                 }
242                 if _, err = fd.Seek(0, io.SeekStart); err != nil {
243                         log.Fatal(err)
244                 }
245                 tgtsRaw, err := io.ReadAll(bufio.NewReader(fd))
246                 if err != nil {
247                         log.Fatal(err)
248                 }
249                 flock.Type = unix.F_UNLCK
250                 if err = unix.FcntlFlock(fdLock.Fd(), unix.F_SETLK, &flock); err != nil {
251                         log.Fatal(err)
252                 }
253                 OODTgts = make(map[string]struct{})
254                 for _, tgtRaw := range bytes.Split(tgtsRaw, []byte{0}) {
255                         t := string(tgtRaw)
256                         if t == "" {
257                                 continue
258                         }
259                         OODTgts[t] = struct{}{}
260                         tracef(CDebug, "ood: known to be: %s", t)
261                 }
262         }
263
264         StderrPrefix = os.Getenv(EnvStderrPrefix)
265         if v := os.Getenv(EnvLevel); v != "" {
266                 Level, err = strconv.Atoi(v)
267                 if err != nil {
268                         panic(err)
269                 }
270                 if Level < 0 {
271                         panic("negative " + EnvLevel)
272                 }
273         }
274
275         var fdDep *os.File
276         if v := os.Getenv(EnvDepFd); v != "" {
277                 fdDep = mustParseFd(v, EnvDepFd)
278         }
279
280         tgts := make([]*Tgt, 0, len(flag.Args()))
281         for _, arg := range flag.Args() {
282                 tgts = append(tgts, NewTgt(arg))
283         }
284         BuildUUID = os.Getenv(EnvBuildUUID)
285         tgtsWasEmpty := len(tgts) == 0
286         if BuildUUID == "" {
287                 IsTopRedo = true
288                 raw := new([16]byte)
289                 if _, err = io.ReadFull(rand.Reader, raw[:]); err != nil {
290                         log.Fatal(err)
291                 }
292                 raw[6] = (raw[6] & 0x0F) | uint8(4<<4) // version 4
293                 BuildUUID = fmt.Sprintf(
294                         "%x-%x-%x-%x-%x",
295                         raw[0:4], raw[4:6], raw[6:8], raw[8:10], raw[10:],
296                 )
297                 if tgtsWasEmpty {
298                         tgts = append(tgts, NewTgt("all"))
299                 }
300                 tracef(CDebug, "inode-trust: %s", InodeTrust)
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(CDebug, "[%s] run: %s %s cwd:%s dirprefix:%s",
324                 BuildUUID, cmdName, tgts, Cwd, DirPrefix)
325
326 CmdSwitch:
327         switch cmdName {
328         case CmdNameRedo:
329                 for _, tgt := range tgts {
330                         ok, err = ifchange([]*Tgt{tgt}, true, traced)
331                         if err != nil || !ok {
332                                 break
333                         }
334                 }
335         case CmdNameRedoIfchange:
336                 ok, err = ifchange(tgts, *flagForcedIfchange, traced)
337                 if err == nil {
338                         err = depsWrite(fdDep, tgts)
339                 }
340         case CmdNameRedoIfcreate:
341                 if fdDep == nil {
342                         log.Fatalln("no", EnvDepFd)
343                 }
344                 for _, tgt := range tgts {
345                         err = ifcreate(fdDep, tgt.RelTo(path.Join(Cwd, DirPrefix)))
346                         if err != nil {
347                                 break
348                         }
349                 }
350         case CmdNameRedoAlways:
351                 if fdDep == nil {
352                         log.Fatalln("no", EnvDepFd)
353                 }
354                 err = always(fdDep)
355         case CmdNameRedoCleanup:
356                 for _, what := range tgts {
357                         err = cleanupWalker(Cwd, what.t)
358                         if err != nil {
359                                 break
360                         }
361                 }
362         case CmdNameRedoDot:
363                 err = dotPrint(tgts)
364         case CmdNameRedoStamp:
365                 if fdDep == nil {
366                         log.Fatalln("no", EnvDepFd)
367                 }
368                 err = stamp(fdDep, os.Stdin)
369         case CmdNameRedoLog:
370                 if len(tgts) != 1 {
371                         log.Fatal("single target expected")
372                 }
373                 err = showBuildLog(tgts[0], nil, 0)
374         case CmdNameRedoWhichdo:
375                 if len(tgts) != 1 {
376                         log.Fatal("single target expected")
377                 }
378                 var fdTmp *os.File
379                 fdTmp, err = os.CreateTemp("", "whichdo")
380                 if err != nil {
381                         err = ErrLine(err)
382                         break
383                 }
384                 err = ErrLine(os.Remove(fdTmp.Name()))
385                 if err != nil {
386                         break
387                 }
388                 tgt := tgts[0]
389                 var doFile string
390                 var upLevels int
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.rel)
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.rel], deps, seen)
461                         }
462                         res = make([]string, 0, len(seen))
463                         for _, dep := range seen {
464                                 res = append(res, dep.rel)
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.rel)
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.rel)
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.rel)
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 }