package main import ( "bufio" "context" "fmt" "io" "log" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "syscall" "time" ) func findRepoRoot() (string, error) { cwd, err := os.Getwd() if err != nil { return "", err } candidates := []string{ cwd, filepath.Clean(filepath.Join(cwd, "..")), } log.Printf("repo root discovery cwd=%s candidates=%v", cwd, candidates) for _, dir := range candidates { if _, err := os.Stat(filepath.Join(dir, "build.zig")); err == nil { log.Printf("repo root found path=%s", dir) return dir, nil } } return "", fmt.Errorf("could not find repo root with build.zig from %s", cwd) } func runBuildInstall(root string, h *hub) error { start := time.Now() cmd := exec.Command("zig", "build", "install") cmd.Dir = root log.Printf("build start cmd=%q dir=%s", strings.Join(cmd.Args, " "), root) h.status("build", "running zig build install") stdout, err := cmd.StdoutPipe() if err != nil { return err } stderr, err := cmd.StderrPipe() if err != nil { return err } if err := cmd.Start(); err != nil { return err } var wg sync.WaitGroup wg.Add(2) go streamPipe("build", "stdout", stdout, h, &wg) go streamPipe("build", "stderr", stderr, h, &wg) wg.Wait() if err := cmd.Wait(); err != nil { log.Printf("build failed duration=%s err=%v", time.Since(start), err) return err } log.Printf("build success duration=%s", time.Since(start)) h.status("build", "zig build install completed") return nil } func streamPipe(procName, streamName string, r io.Reader, h *hub, wg *sync.WaitGroup) { defer wg.Done() scanner := bufio.NewScanner(r) for scanner.Scan() { h.broadcast(logMessage{ Process: procName, Stream: streamName, Line: scanner.Text(), Time: time.Now().Format(time.RFC3339), }) } if err := scanner.Err(); err != nil { log.Printf("stream scanner error process=%s stream=%s err=%v", procName, streamName, err) } } func launchProcess(ctx context.Context, root, name, binPath string, args []string, h *hub) (*exec.Cmd, error) { log.Printf("launch requested name=%s bin=%s args=%v dir=%s", name, binPath, args, root) cmd := exec.CommandContext(ctx, binPath, args...) cmd.Dir = root stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } stderr, err := cmd.StderrPipe() if err != nil { return nil, err } if err := cmd.Start(); err != nil { return nil, err } h.status(name, fmt.Sprintf("started pid=%d args=%v", cmd.Process.Pid, args)) var wg sync.WaitGroup wg.Add(2) go streamPipe(name, "stdout", stdout, h, &wg) go streamPipe(name, "stderr", stderr, h, &wg) go func() { wg.Wait() }() return cmd, nil } func stopProcess(cmd *exec.Cmd) { if cmd == nil || cmd.Process == nil { return } if cmd.ProcessState != nil && cmd.ProcessState.Exited() { return } if runtime.GOOS == "windows" { _ = cmd.Process.Kill() return } _ = cmd.Process.Signal(syscall.SIGTERM) done := make(chan struct{}) go func() { _, _ = cmd.Process.Wait() close(done) }() select { case <-done: case <-time.After(2 * time.Second): _ = cmd.Process.Kill() } } func mustBinPath(root, name string) (string, error) { suffix := "" if runtime.GOOS == "windows" { suffix = ".exe" } p := filepath.Join(root, "zig-out", "bin", name+suffix) if _, err := os.Stat(p); err != nil { return "", fmt.Errorf("missing binary %s: %w", p, err) } return p, nil }