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