2 goredo -- redo implementation on pure Go
3 Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
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.
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.
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/>.
36 "go.cypherpunks.ru/recfile"
37 "golang.org/x/sys/unix"
48 func mkdirs(pth string) error {
49 if _, err := os.Stat(pth); err == nil {
52 return os.MkdirAll(pth, os.FileMode(0777))
55 func tempsuffix() string {
56 return strconv.FormatInt((time.Now().UnixNano()+int64(os.Getpid()))&0xFFFFFFFF, 16)
59 func tempfile(dir, prefix string) (*os.File, error) {
60 // It respects umask, unlike ioutil.TempFile
61 name := path.Join(dir, TmpPrefix+prefix+"."+tempsuffix())
62 return os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
65 func isModified(cwd, redoDir, tgt string) (bool, error) {
66 fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
68 if os.IsNotExist(err) {
74 r := recfile.NewReader(fdDep)
83 if m["Target"] != tgt {
86 fd, err := os.Open(path.Join(cwd, tgt))
88 if os.IsNotExist(err) {
94 ourTs, err := fileCtime(fd)
98 if ourTs != m["Ctime"] {
106 func runScript(tgt string, errs chan error) error {
108 cwd, tgt := cwdAndTgt(tgt)
109 redoDir := path.Join(cwd, RedoDir)
110 errf := func(err error) error {
111 return fmt.Errorf("%s: %s", tgtOrig, err)
113 if err := mkdirs(redoDir); err != nil {
118 fdLock, err := os.OpenFile(
119 path.Join(redoDir, "lock."+tgt),
120 os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
126 lockRelease := func() {
127 trace(CLock, "LOCK_UN: %s", fdLock.Name())
128 unix.Flock(int(fdLock.Fd()), unix.LOCK_UN)
131 trace(CLock, "LOCK_NB: %s", fdLock.Name())
132 if err = unix.Flock(int(fdLock.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
133 if uintptr(err.(syscall.Errno)) != uintptr(unix.EWOULDBLOCK) {
137 trace(CDebug, "waiting: %s", tgtOrig)
141 trace(CLock, "LOCK_EX: %s", fdLock.Name())
142 unix.Flock(int(fdLock.Fd()), unix.LOCK_EX)
144 trace(CDebug, "waiting done: %s", tgtOrig)
146 fdDep, err := os.Open(path.Join(redoDir, tgt+DepSuffix))
148 if os.IsNotExist(err) {
149 err = fmt.Errorf("%s is not built", tgtOrig)
153 builtNow, _, err = isBuiltNow(fdDep)
158 err = fmt.Errorf("%s is not built", tgtOrig)
169 // Check if target is not modified externally
170 modified, err := isModified(cwd, redoDir, tgt)
176 trace(CWarn, "%s externally modified: not redoing", tgtOrig)
186 // Start preparing dep.
187 fdDep, err := tempfile(redoDir, tgt+DepSuffix)
195 os.Remove(fdDep.Name())
197 if _, err = recfile.NewWriter(fdDep).WriteFields(
198 recfile.Field{Name: "Build", Value: BuildUUID},
205 doFile, upLevels, err := findDo(fdDep, cwd, tgt)
212 return errf(errors.New("no .do found"))
214 if err = writeDep(fdDep, cwd, doFile); err != nil {
219 // Determine basename and DIRPREFIX
220 ents := strings.Split(cwd, "/")
221 ents = ents[len(ents)-upLevels:]
222 dirPrefix := path.Join(ents...)
224 for i := 0; i < upLevels; i++ {
225 cwd = path.Join(cwd, "..")
227 cwd = path.Clean(cwd)
229 if strings.HasPrefix(doFile, "default.") {
230 basename = tgt[:len(tgt)-(len(doFile)-len("default.")-len(".do"))-1]
231 trace(CRedo, "%s (%s)", tgtOrig, doFile)
233 trace(CRedo, "%s", tgtOrig)
235 doFile = path.Base(doFile)
237 // Prepare command line
240 if err = unix.Access(path.Join(cwd, doFile), unix.X_OK); err == nil {
241 // Ordinary executable file
243 args = make([]string, 0, 3)
245 fd, err := os.Open(path.Join(cwd, doFile))
250 buf := make([]byte, 512)
251 n, err := fd.Read(buf)
256 if n > 3 && string(buf[:3]) == "#!/" {
258 t := string(buf[2:n])
259 nlIdx := strings.Index(t, "\n")
262 return errf(errors.New("not fully read shebang"))
264 args = strings.Split(t[:nlIdx], " ")
265 cmdName, args = args[0], args[1:]
270 args = append(args, "-ex")
272 args = append(args, "-e")
275 args = append(args, doFile)
278 // Temporary file for stdout
279 fdStdout, err := tempfile(cwd, tgt)
284 tmpPath := fdStdout.Name() + ".3" // and for $3
285 args = append(args, tgt, basename, path.Base(tmpPath))
287 cmd := exec.Command(cmdName, args...)
289 cmd.Stdout = fdStdout
290 cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%d", RedoLevelEnv, Level+1))
291 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", RedoDirPrefixEnv, dirPrefix))
292 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", RedoBuildUUIDEnv, BuildUUID))
293 childStderrPrefix := tempsuffix()
294 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", RedoStderrPrefixEnv, childStderrPrefix))
295 cmd.ExtraFiles = append(cmd.ExtraFiles, fdDep)
296 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoDepFdEnv, 3+0))
299 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", RedoJSRFdEnv))
300 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=NO", RedoJSWFdEnv))
302 cmd.ExtraFiles = append(cmd.ExtraFiles, JSR)
303 cmd.ExtraFiles = append(cmd.ExtraFiles, JSW)
304 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoJSRFdEnv, 3+1))
305 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", RedoJSWFdEnv, 3+2))
309 stderr, err := cmd.StderrPipe()
313 var fdStderr *os.File
315 fdStderr, err = os.OpenFile(
316 path.Join(redoDir, tgt+LogSuffix),
317 os.O_WRONLY|os.O_CREATE,
326 trace(CDebug, "sh: %s: %s %s [%s]", tgtOrig, cmdName, args, cwd)
331 started := time.Now()
340 os.Remove(fdDep.Name())
341 os.Remove(fdStdout.Name())
343 os.Remove(fdLock.Name())
344 finished := time.Now()
345 trace(CDone, "%s (%fsec)", tgtOrig, finished.Sub(started).Seconds())
353 pid := fmt.Sprintf("[%d]", cmd.Process.Pid)
354 trace(CDebug, "%s runs %s", tgtOrig, pid)
356 stderrTerm := make(chan struct{}, 0)
358 scanner := bufio.NewScanner(stderr)
362 line = scanner.Text()
363 if strings.HasPrefix(line, childStderrPrefix) {
364 line = line[len(childStderrPrefix):]
365 os.Stderr.WriteString(StderrPrefix + line + "\n")
370 fmt.Fprintf(fdStderr, "@%s %s\n", hex.EncodeToString(ts[:]), line)
376 trace(CNone, "%s", line)
378 trace(CNone, "%s %s", pid, line)
384 // Wait for job completion
392 // Does it produce both stdout and tmp?
393 fiStdout, err := os.Stat(fdStdout.Name())
399 _, err = os.Stat(tmpPath)
401 if fiStdout.Size() > 0 {
402 errs <- errf(fmt.Errorf("%s created both tmp and stdout", tgtOrig))
406 } else if !os.IsNotExist(err) {
411 // Determine what file we must process at last
414 fd, err = os.Open(tmpPath)
420 } else if fiStdout.Size() > 0 {
424 // Do we need to ifcreate it, of ifchange with renaming?
426 if err = ifcreate(fdDep, tgt); err != nil {
432 if err = fd.Sync(); err != nil {
437 if err = os.Rename(fd.Name(), path.Join(cwdOrig, tgt)); err != nil {
442 fdDir, err := os.Open(cwdOrig)
448 if err = fdDir.Sync(); err != nil {
453 if err = writeDep(fdDep, cwdOrig, tgt); err != nil {
461 if err = fdDep.Sync(); err != nil {
466 if err = os.Rename(fdDep.Name(), path.Join(redoDir, tgt+DepSuffix)); err != nil {
471 fdDir, err := os.Open(redoDir)
477 if err = fdDir.Sync(); err != nil {