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