]> Cypherpunks.ru repositories - goredo.git/blob - main.go
03a73d448334295d8aa4e7a04f5a6b46c59f9a88
[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         "encoding/hex"
24         "flag"
25         "fmt"
26         "io"
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 == "goredo" {
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                         break
396                 }
397                 sort.Strings(dos)
398                 for _, do := range dos {
399                         fmt.Println(do)
400                 }
401         case CmdNameRedoTargets:
402                 raws := make([]string, 0, len(tgts))
403                 for _, tgt := range tgts {
404                         raws = append(raws, tgt.rel)
405                 }
406                 if tgtsWasEmpty {
407                         raws = []string{Cwd}
408                 }
409                 raws, err = targetsWalker(raws)
410                 if err != nil {
411                         err = ErrLine(err)
412                         break
413                 }
414                 sort.Strings(raws)
415                 for _, tgt := range raws {
416                         fmt.Println(tgt)
417                 }
418         case CmdNameRedoAffects:
419                 if tgtsWasEmpty {
420                         log.Fatal("no targets specified")
421                 }
422                 var res []string
423                 {
424                         var tgtsKnown []string
425                         tgtsKnown, err = targetsWalker([]string{Cwd})
426                         if err != nil {
427                                 err = ErrLine(err)
428                                 break
429                         }
430                         deps := make(map[string]map[string]*Tgt)
431                         for _, tgt := range tgtsKnown {
432                                 collectDeps(NewTgt(tgt), 0, deps, true)
433                         }
434                         seen := make(map[string]*Tgt)
435                         for _, tgt := range tgts {
436                                 collectWholeDeps(deps[tgt.rel], deps, seen)
437                         }
438                         res = make([]string, 0, len(seen))
439                         for _, dep := range seen {
440                                 res = append(res, dep.rel)
441                         }
442                 }
443                 sort.Strings(res)
444                 for _, dep := range res {
445                         fmt.Println(dep)
446                 }
447         case CmdNameRedoOOD:
448                 raws := make([]string, 0, len(tgts))
449                 for _, tgt := range tgts {
450                         raws = append(raws, tgt.rel)
451                 }
452                 if tgtsWasEmpty {
453                         raws, err = targetsWalker([]string{Cwd})
454                         if err != nil {
455                                 break
456                         }
457                 }
458                 sort.Strings(raws)
459                 var ood bool
460                 for _, tgt := range raws {
461                         ood, err = isOOD(NewTgt(tgt), 0, nil)
462                         if err != nil {
463                                 err = ErrLine(err)
464                                 break
465                         }
466                         if ood {
467                                 fmt.Println(tgt)
468                         }
469                 }
470         case CmdNameRedoSources:
471                 srcs := make(map[string]*Tgt)
472                 {
473                         raws := make([]string, 0, len(tgts))
474                         for _, tgt := range tgts {
475                                 raws = append(raws, tgt.rel)
476                         }
477                         if tgtsWasEmpty {
478                                 raws, err = targetsWalker([]string{Cwd})
479                                 if err != nil {
480                                         err = ErrLine(err)
481                                         break
482                                 }
483                         }
484                         sort.Strings(raws)
485                         tgts = tgts[:0]
486                         for _, raw := range raws {
487                                 tgts = append(tgts, NewTgt(raw))
488                         }
489                         seen := make(map[string]struct{})
490                         seenDeps := make(map[string]struct{})
491                         err = ErrLine(sourcesWalker(tgts, seen, seenDeps, srcs))
492                 }
493                 if err != nil {
494                         break
495                 }
496                 res := make([]string, 0, len(srcs))
497                 for _, tgt := range srcs {
498                         res = append(res, tgt.rel)
499                 }
500                 srcs = nil
501                 sort.Strings(res)
502                 for _, src := range res {
503                         fmt.Println(src)
504                 }
505         case CmdNameRedoDepFix:
506                 IfchangeCache = nil
507                 DepFixHashCache = make(map[string]Hash)
508                 err = depFix(Cwd)
509         case CmdNameRedoInode:
510                 var inode *Inode
511                 for _, tgt := range tgts {
512                         inode, err = inodeFromFileByPath(tgt.a)
513                         if err != nil {
514                                 err = ErrLine(err)
515                                 break
516                         }
517                         err = recfileWrite(os.Stdout, append(
518                                 []recfile.Field{{Name: "Target", Value: tgt.String()}},
519                                 inode.RecfileFields()...)...)
520                         if err != nil {
521                                 err = ErrLine(err)
522                                 break
523                         }
524                 }
525         case CmdNameRedoDep2Rec:
526                 var data []byte
527                 data, err = os.ReadFile(tgts[0].a)
528                 if err != nil {
529                         break
530                 }
531                 var build uuid.UUID
532                 build, data, err = depHeadParse(data)
533                 if err != nil {
534                         break
535                 }
536                 w := bufio.NewWriter(os.Stdout)
537                 err = recfileWrite(w, []recfile.Field{
538                         {Name: "Build", Value: build.String()},
539                 }...)
540                 if err != nil {
541                         break
542                 }
543                 var typ byte
544                 var chunk []byte
545                 var inode Inode
546                 for len(data) > 0 {
547                         typ, chunk, data, _ = chunkRead(data)
548                         switch typ {
549                         case DepTypeAlways:
550                                 err = recfileWrite(w, []recfile.Field{
551                                         {Name: "Type", Value: "always"},
552                                 }...)
553                         case DepTypeStamp:
554                                 err = recfileWrite(w, []recfile.Field{
555                                         {Name: "Type", Value: "stamp"},
556                                         {Name: "Hash", Value: hex.EncodeToString(chunk)},
557                                 }...)
558                         case DepTypeIfcreate:
559                                 err = recfileWrite(w, []recfile.Field{
560                                         {Name: "Type", Value: "ifcreate"},
561                                         {Name: "Target", Value: string(chunk)},
562                                 }...)
563                         case DepTypeIfchange:
564                                 name := string(chunk[InodeLen+HashLen:])
565                                 meta := chunk[:InodeLen+HashLen]
566                                 fields := []recfile.Field{
567                                         {Name: "Type", Value: "ifchange"},
568                                         {Name: "Target", Value: name},
569                                 }
570                                 fields = append(fields, recfile.Field{
571                                         Name: "Hash", Value: Hash(meta[InodeLen:]).String(),
572                                 })
573                                 inode = Inode(meta[:][:InodeLen])
574                                 fields = append(fields, inode.RecfileFields()...)
575                                 err = recfileWrite(w, fields...)
576                         case DepTypeIfchangeNonex:
577                                 err = recfileWrite(w, []recfile.Field{
578                                         {Name: "Type", Value: "ifchange"},
579                                         {Name: "Target", Value: string(chunk)},
580                                 }...)
581                         }
582                         if err != nil {
583                                 break
584                         }
585                 }
586                 err = w.Flush()
587         default:
588                 log.Fatalln("unknown command", cmdName)
589         }
590         if err != nil {
591                 log.Print(err)
592         }
593         rc := 0
594         if !ok || err != nil {
595                 rc = 1
596         }
597         tracef(CDebug, "[%s] finished: %s %s", BuildUUID, cmdName, tgts)
598         os.Exit(rc)
599 }