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