]> Cypherpunks.ru repositories - goredo.git/blob - run.go
Correct relative dependency paths
[goredo.git] / run.go
1 /*
2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2021 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 // Targets runner
19
20 package main
21
22 import (
23         "bufio"
24         "errors"
25         "flag"
26         "fmt"
27         "io"
28         "os"
29         "os/exec"
30         "path"
31         "path/filepath"
32         "strings"
33         "sync"
34         "syscall"
35         "time"
36
37         "go.cypherpunks.ru/recfile"
38         "go.cypherpunks.ru/tai64n/v2"
39         "golang.org/x/sys/unix"
40 )
41
42 const (
43         EnvDepFd        = "REDO_DEP_FD"
44         EnvDirPrefix    = "REDO_DIRPREFIX"
45         EnvDepCwd       = "REDO_DEP_CWD"
46         EnvBuildUUID    = "REDO_BUILD_UUID"
47         EnvStderrPrefix = "REDO_STDERR_PREFIX"
48         EnvTrace        = "REDO_TRACE"
49         EnvStderrKeep   = "REDO_LOGS"
50         EnvStderrSilent = "REDO_SILENT"
51         EnvNoSync       = "REDO_NO_SYNC"
52
53         RedoDir    = ".redo"
54         LockSuffix = ".lock"
55         DepSuffix  = ".rec"
56         TmpPrefix  = ".redo."
57         LogSuffix  = ".log"
58 )
59
60 var (
61         NoSync       bool = false
62         StderrKeep   bool = false
63         StderrSilent bool = false
64         StderrPrefix string
65         Jobs         sync.WaitGroup
66
67         flagTrace        = flag.Bool("x", false, "trace (sh -x) current targets")
68         flagTraceAll     = flag.Bool("xx", false, fmt.Sprintf("trace (sh -x) all targets (%s=1)", EnvTrace))
69         flagStderrKeep   = flag.Bool("logs", false, fmt.Sprintf("keep job's stderr (%s=1)", EnvStderrKeep))
70         flagStderrSilent = flag.Bool("silent", false, fmt.Sprintf("do not print job's stderr (%s=1)", EnvStderrSilent))
71
72         TracedAll bool
73 )
74
75 type RunErr struct {
76         Tgt      string
77         DoFile   string
78         Started  *time.Time
79         Finished *time.Time
80         Err      error
81 }
82
83 func (e *RunErr) Name() string {
84         var name string
85         if e.DoFile == "" {
86                 name = e.Tgt
87         } else {
88                 name = fmt.Sprintf("%s (%s)", e.Tgt, e.DoFile)
89         }
90         if e.Finished == nil {
91                 return name
92         }
93         return fmt.Sprintf("%s (%.3fs)", name, e.Finished.Sub(*e.Started).Seconds())
94 }
95
96 func (e RunErr) Error() string {
97         return fmt.Sprintf("%s: %s", e.Name(), e.Err)
98 }
99
100 func mkdirs(pth string) error {
101         if _, err := os.Stat(pth); err == nil {
102                 return nil
103         }
104         return os.MkdirAll(pth, os.FileMode(0777))
105 }
106
107 func isModified(cwd, redoDir, tgt string) (bool, *Inode, error) {
108         fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
109         if err != nil {
110                 if os.IsNotExist(err) {
111                         return false, nil, nil
112                 }
113                 return false, nil, err
114         }
115         defer fdDep.Close()
116         r := recfile.NewReader(fdDep)
117         var ourInode *Inode
118         for {
119                 m, err := r.NextMap()
120                 if err != nil {
121                         if err == io.EOF {
122                                 break
123                         }
124                         return false, nil, err
125                 }
126                 if m["Target"] != tgt {
127                         continue
128                 }
129                 fd, err := os.Open(path.Join(cwd, tgt))
130                 if err != nil {
131                         if os.IsNotExist(err) {
132                                 return false, nil, nil
133                         }
134                         return false, nil, err
135                 }
136                 ourInode, err = inodeFromFile(fd)
137                 fd.Close()
138                 if err != nil {
139                         return false, nil, err
140                 }
141                 theirInode, err := inodeFromRec(m)
142                 if err != nil {
143                         return false, nil, err
144                 }
145                 if !ourInode.Equals(theirInode) {
146                         return true, ourInode, nil
147                 }
148                 break
149         }
150         return false, ourInode, nil
151 }
152
153 func syncDir(dir string) error {
154         fd, err := os.Open(dir)
155         if err != nil {
156                 return err
157         }
158         err = fd.Sync()
159         fd.Close()
160         return err
161 }
162
163 func runScript(tgtOrig string, errs chan error, traced bool) error {
164         cwd, tgt := cwdAndTgt(tgtOrig)
165         redoDir := path.Join(cwd, RedoDir)
166         if err := mkdirs(redoDir); err != nil {
167                 return TgtErr{tgtOrig, err}
168         }
169
170         // Acquire lock
171         fdLock, err := os.OpenFile(
172                 path.Join(redoDir, tgt+LockSuffix),
173                 os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
174                 os.FileMode(0666),
175         )
176         if err != nil {
177                 return TgtErr{tgtOrig, err}
178         }
179         lockRelease := func() {
180                 trace(CLock, "LOCK_UN: %s", fdLock.Name())
181                 unix.Flock(int(fdLock.Fd()), unix.LOCK_UN)
182                 fdLock.Close()
183         }
184         trace(CLock, "LOCK_NB: %s", fdLock.Name())
185
186         // Waiting for job completion, already taken by someone else
187         if err = unix.Flock(int(fdLock.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
188                 if uintptr(err.(syscall.Errno)) != uintptr(unix.EWOULDBLOCK) {
189                         fdLock.Close()
190                         return TgtErr{tgtOrig, err}
191                 }
192                 Jobs.Add(1)
193                 trace(CDebug, "waiting: %s", tgtOrig)
194                 if FdStatus != nil {
195                         FdStatus.Write([]byte{StatusWait})
196                 }
197                 go func() {
198                         defer Jobs.Done()
199                         trace(CLock, "LOCK_EX: %s", fdLock.Name())
200                         unix.Flock(int(fdLock.Fd()), unix.LOCK_EX)
201                         lockRelease()
202                         trace(CDebug, "waiting done: %s", tgtOrig)
203                         if FdStatus != nil {
204                                 FdStatus.Write([]byte{StatusWaited})
205                         }
206                         var depInfo *DepInfo
207                         fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
208                         if err != nil {
209                                 if os.IsNotExist(err) {
210                                         err = errors.New("was not built: no .rec")
211                                 }
212                                 goto Finish
213                         }
214                         defer fdDep.Close()
215                         depInfo, err = depRead(fdDep)
216                         if err != nil {
217                                 goto Finish
218                         }
219                         if depInfo.build != BuildUUID {
220                                 err = errors.New("was not built: build differs")
221                         }
222                 Finish:
223                         if err != nil {
224                                 err = TgtErr{tgtOrig, err}
225                         }
226                         errs <- err
227                 }()
228                 return nil
229         }
230
231         // Check if target is not modified externally
232         modified, inodePrev, err := isModified(cwd, redoDir, tgt)
233         if err != nil {
234                 lockRelease()
235                 return TgtErr{tgtOrig, err}
236         }
237         if modified {
238                 trace(CWarn, "%s externally modified: not redoing", tgtOrig)
239                 lockRelease()
240                 go func() {
241                         errs <- nil
242                 }()
243                 return nil
244         }
245
246         // Start preparing .rec
247         fdDep, err := tempfile(redoDir, tgt+DepSuffix)
248         if err != nil {
249                 lockRelease()
250                 return TgtErr{tgtOrig, err}
251         }
252         fdDepPath := fdDep.Name()
253         cleanup := func() {
254                 lockRelease()
255                 fdDep.Close()
256                 os.Remove(fdDep.Name())
257         }
258         if _, err = recfile.NewWriter(fdDep).WriteFields(
259                 recfile.Field{Name: "Build", Value: BuildUUID},
260         ); err != nil {
261                 cleanup()
262                 return TgtErr{tgtOrig, err}
263         }
264
265         // Find .do
266         doFile, upLevels, err := findDo(fdDep, cwd, tgt)
267         if err != nil {
268                 cleanup()
269                 return TgtErr{tgtOrig, err}
270         }
271         if doFile == "" {
272                 cleanup()
273                 return TgtErr{tgtOrig, errors.New("no .do found")}
274         }
275
276         // Determine basename and DIRPREFIX
277         doFileRelPath := doFile
278         ents := strings.Split(cwd, "/")
279         ents = ents[len(ents)-upLevels:]
280         dirPrefix := path.Join(ents...)
281         cwdOrig := cwd
282         for i := 0; i < upLevels; i++ {
283                 cwd = path.Join(cwd, "..")
284                 doFileRelPath = path.Join("..", doFileRelPath)
285         }
286         cwd = path.Clean(cwd)
287         doFilePath := path.Join(cwd, doFile)
288         basename := tgt
289         runErr := RunErr{Tgt: tgtOrig}
290         if strings.HasPrefix(doFile, "default.") {
291                 basename = tgt[:len(tgt)-(len(doFile)-len("default.")-len(".do"))-1]
292                 runErr.DoFile = doFileRelPath
293         }
294
295         if err = writeDep(fdDep, cwdOrig, doFileRelPath); err != nil {
296                 cleanup()
297                 return TgtErr{tgtOrig, err}
298         }
299         fdDep.Close()
300         trace(CWait, "%s", runErr.Name())
301
302         // Prepare command line
303         var cmdName string
304         var args []string
305         if err = unix.Access(doFilePath, unix.X_OK); err == nil {
306                 cmdName = doFilePath
307                 args = make([]string, 0, 3)
308         } else {
309                 cmdName = "/bin/sh"
310                 if traced || TracedAll {
311                         args = append(args, "-ex")
312                 } else {
313                         args = append(args, "-e")
314                 }
315                 args = append(args, doFile)
316         }
317
318         // Temporary file for stdout
319         fdStdout, err := tempfile(cwdOrig, tgt)
320         if err != nil {
321                 cleanup()
322                 return TgtErr{tgtOrig, err}
323         }
324         stdoutPath := fdStdout.Name()
325         fdStdout.Close()
326         tmpPath := stdoutPath + ".3" // and for $3
327         tmpPathRel, err := filepath.Rel(cwd, tmpPath)
328         if err != nil {
329                 panic(err)
330         }
331         args = append(
332                 args,
333                 path.Join(dirPrefix, tgt),
334                 path.Join(dirPrefix, basename),
335                 tmpPathRel,
336         )
337
338         cmd := exec.Command(cmdName, args...)
339         cmd.Dir = cwd
340         // cmd.Stdin reads from /dev/null by default
341         cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%d", EnvLevel, Level+1))
342         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvDirPrefix, dirPrefix))
343         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvBuildUUID, BuildUUID))
344
345         childStderrPrefix := tempsuffix()
346         cmd.Env = append(cmd.Env, fmt.Sprintf(
347                 "%s=%s", EnvStderrPrefix, childStderrPrefix,
348         ))
349
350         fdNum := 0
351         cmd.ExtraFiles = append(cmd.ExtraFiles, FdOODTgts)
352         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvOODTgtsFd, 3+fdNum))
353         fdNum++
354         cmd.ExtraFiles = append(cmd.ExtraFiles, FdOODTgtsLock)
355         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvOODTgtsLockFd, 3+fdNum))
356         fdNum++
357
358         if FdStatus == nil {
359                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvStatusFd))
360         } else {
361                 cmd.ExtraFiles = append(cmd.ExtraFiles, FdStatus)
362                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvStatusFd, 3+fdNum))
363                 fdNum++
364         }
365
366         // Preparing stderr
367         var fdStderr *os.File
368         if StderrKeep {
369                 fdStderr, err = os.OpenFile(
370                         path.Join(redoDir, tgt+LogSuffix),
371                         os.O_WRONLY|os.O_CREATE,
372                         os.FileMode(0666),
373                 )
374                 if err != nil {
375                         cleanup()
376                         return TgtErr{tgtOrig, err}
377                 }
378                 fdStderr.Truncate(0)
379         }
380         shCtx := fmt.Sprintf(
381                 "sh: %s: %s %s cwd:%s dirprefix:%s",
382                 tgtOrig, cmdName, args, cwd, dirPrefix,
383         )
384         trace(CDebug, "%s", shCtx)
385
386         Jobs.Add(1)
387         go func() {
388                 jsToken := jsAcquire(shCtx)
389                 if JSR == nil {
390                         // infinite jobs
391                         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", EnvJobs))
392                 } else {
393                         cmd.ExtraFiles = append(cmd.ExtraFiles, JSR)
394                         cmd.ExtraFiles = append(cmd.ExtraFiles, JSW)
395                         cmd.Env = append(cmd.Env, fmt.Sprintf(
396                                 "%s=%s %s%d,%d",
397                                 MakeFlagsName, MakeFlags, MakeJSArg, 3+fdNum+0, 3+fdNum+1,
398                         ))
399                         fdNum += 2
400                         cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvJSToken, jsToken))
401                 }
402
403                 if FdStatus != nil {
404                         FdStatus.Write([]byte{StatusRun})
405                 }
406
407                 started := time.Now()
408                 runErr.Started = &started
409                 fdStdout, err = os.OpenFile(stdoutPath, os.O_RDWR, os.FileMode(0666))
410                 if err != nil {
411                         runErr.Err = err
412                         errs <- runErr
413                         return
414                 }
415                 cmd.Stdout = fdStdout
416                 fdDep, err = os.OpenFile(fdDepPath, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
417                 if err != nil {
418                         runErr.Err = err
419                         errs <- runErr
420                         return
421                 }
422                 cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep)
423                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", EnvDepFd, 3+fdNum))
424                 fdNum++
425                 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", EnvDepCwd, cwd))
426
427                 defer func() {
428                         jsRelease(shCtx, jsToken)
429                         lockRelease()
430                         fdDep.Close()
431                         fdStdout.Close()
432                         if fdStderr != nil {
433                                 fdStderr.Close()
434                         }
435                         os.Remove(fdDep.Name())
436                         os.Remove(fdStdout.Name())
437                         os.Remove(tmpPath)
438                         os.Remove(fdLock.Name())
439                         if FdStatus != nil {
440                                 FdStatus.Write([]byte{StatusDone})
441                         }
442                         Jobs.Done()
443                 }()
444                 stderr, err := cmd.StderrPipe()
445                 if err != nil {
446                         runErr.Err = err
447                         errs <- runErr
448                         return
449                 }
450                 started = time.Now()
451                 err = cmd.Start()
452                 if err != nil {
453                         runErr.Err = err
454                         errs <- runErr
455                         return
456                 }
457                 pid := fmt.Sprintf("[%d]", cmd.Process.Pid)
458                 trace(CDebug, "%s runs %s", tgtOrig, pid)
459
460                 stderrTerm := make(chan struct{}, 0)
461                 go func() {
462                         scanner := bufio.NewScanner(stderr)
463                         var line string
464                         ts := new(tai64n.TAI64N)
465                         for scanner.Scan() {
466                                 line = scanner.Text()
467                                 if strings.HasPrefix(line, childStderrPrefix) {
468                                         line = line[len(childStderrPrefix):]
469                                         os.Stderr.WriteString(StderrPrefix + line + "\n")
470                                         continue
471                                 }
472                                 if fdStderr != nil {
473                                         ts.FromTime(time.Now())
474                                         LogMutex.Lock()
475                                         fmt.Fprintln(fdStderr, tai64n.Encode(ts[:]), line)
476                                         LogMutex.Unlock()
477                                 }
478                                 if StderrSilent {
479                                         continue
480                                 }
481                                 if MyPid == 0 {
482                                         trace(CNone, "%s", line)
483                                 } else {
484                                         trace(CNone, "%s %s", pid, line)
485                                 }
486                         }
487                         close(stderrTerm)
488                 }()
489
490                 // Wait for job completion
491                 <-stderrTerm
492                 err = cmd.Wait()
493                 finished := time.Now()
494                 runErr.Finished = &finished
495                 if err != nil {
496                         runErr.Err = err
497                         errs <- runErr
498                         return
499                 }
500
501                 // Was $1 touched?
502                 if inodePrev != nil {
503                         if fd, err := os.Open(path.Join(cwdOrig, tgt)); err == nil {
504                                 inode, err := inodeFromFile(fd)
505                                 fd.Close()
506                                 if err == nil && !inode.Equals(inodePrev) {
507                                         runErr.Err = errors.New("$1 was explicitly touched")
508                                         errs <- runErr
509                                         return
510                                 }
511                         }
512                 }
513
514                 // Does it produce both stdout and tmp?
515                 fiStdout, err := os.Stat(fdStdout.Name())
516                 if err != nil {
517                         runErr.Err = err
518                         errs <- runErr
519                         return
520                 }
521                 tmpExists := false
522                 _, err = os.Stat(tmpPath)
523                 if err == nil {
524                         if fiStdout.Size() > 0 {
525                                 runErr.Err = errors.New("created both tmp and stdout")
526                                 errs <- runErr
527                                 return
528                         }
529                         tmpExists = true
530                 } else if !os.IsNotExist(err) {
531                         runErr.Err = err
532                         errs <- runErr
533                         return
534                 }
535
536                 // Determine what file we must process at last
537                 var fd *os.File
538                 if tmpExists {
539                         fd, err = os.Open(tmpPath)
540                         if err != nil {
541                                 goto Finish
542                         }
543                         defer fd.Close()
544                 } else if fiStdout.Size() > 0 {
545                         fd = fdStdout
546                 }
547
548                 // Do we need to ifcreate it, of ifchange with renaming?
549                 if fd == nil {
550                         os.Remove(path.Join(cwdOrig, tgt))
551                         err = ifcreate(fdDep, tgt)
552                         if err != nil {
553                                 goto Finish
554                         }
555                 } else {
556                         if !NoSync {
557                                 err = fd.Sync()
558                                 if err != nil {
559                                         goto Finish
560                                 }
561                         }
562                         err = os.Rename(fd.Name(), path.Join(cwdOrig, tgt))
563                         if err != nil {
564                                 goto Finish
565                         }
566                         if !NoSync {
567                                 err = syncDir(cwdOrig)
568                                 if err != nil {
569                                         goto Finish
570                                 }
571                         }
572                         err = writeDep(fdDep, cwdOrig, tgt)
573                         if err != nil {
574                                 goto Finish
575                         }
576                 }
577
578                 // Commit .rec
579                 if !NoSync {
580                         err = fdDep.Sync()
581                         if err != nil {
582                                 goto Finish
583                         }
584                 }
585                 err = os.Rename(fdDep.Name(), path.Join(redoDir, tgt+DepSuffix))
586                 if err != nil {
587                         goto Finish
588                 }
589                 if !NoSync {
590                         err = syncDir(redoDir)
591                         if err != nil {
592                                 goto Finish
593                         }
594                 }
595         Finish:
596                 runErr.Err = err
597                 errs <- runErr
598         }()
599         return nil
600 }
601
602 func isOkRun(err error) bool {
603         if err == nil {
604                 return true
605         }
606         if err, ok := err.(RunErr); ok && err.Err == nil {
607                 trace(CRedo, "%s", err.Name())
608                 return true
609         }
610         trace(CErr, "%s", err)
611         return false
612 }