package main import ( "flag" "fmt" "net/http" "os" "os/exec" "path/filepath" "strings" "time" ) func main() { var frontendDev string var frontendURL string var readyTimeout time.Duration var watchTarget string var workspaceDir string flag.StringVar(&frontendDev, "frontend-dev", "", "") flag.StringVar(&frontendURL, "frontend-url", "http://127.0.0.1:9245", "") flag.DurationVar(&readyTimeout, "ready-timeout", 2*time.Minute, "") flag.StringVar(&watchTarget, "watch-target", "", "") flag.StringVar(&workspaceDir, "workspace-dir", ".", "") flag.Parse() require(frontendDev != "", "missing --frontend-dev") require(watchTarget != "", "missing --watch-target") workspaceRoot := os.Getenv("BUILD_WORKSPACE_DIRECTORY") if workspaceRoot == "" { workspaceRoot = "." } runfilesDir := resolveRunfilesDir() frontendCommand := exec.Command(frontendDev) frontendCommand.Dir = workspaceRoot frontendCommand.Env = withRunfilesEnv(os.Environ(), runfilesDir) frontendCommand.Stdout = os.Stdout frontendCommand.Stderr = os.Stderr must(frontendCommand.Start()) waitCh := make(chan error, 1) go func() { waitCh <- frontendCommand.Wait() }() defer terminate(frontendCommand, waitCh) must(waitForURL(frontendURL, readyTimeout, waitCh)) watchCommand, err := resolveWatchCommand(watchTarget) must(err) watchCommand.Dir = workspaceRoot watchCommand.Env = os.Environ() watchCommand.Stdout = os.Stdout watchCommand.Stderr = os.Stderr watchCommand.Stdin = os.Stdin if err := watchCommand.Run(); err != nil { if exitError, ok := err.(*exec.ExitError); ok { os.Exit(exitError.ExitCode()) } panic(err) } _ = workspaceDir } func resolveRunfilesDir() string { for _, candidate := range []string{ os.Getenv("RUNFILES_DIR"), os.Getenv("RUNFILES"), } { if candidate != "" { return candidate } } executablePath, err := os.Executable() if err != nil { return "" } normalizedPath := filepath.Clean(executablePath) marker := ".runfiles" if index := strings.Index(normalizedPath, marker+string(os.PathSeparator)); index >= 0 { return normalizedPath[:index+len(marker)] } if strings.HasSuffix(normalizedPath, marker) { return normalizedPath } return "" } func withRunfilesEnv(environment []string, runfilesDir string) []string { if runfilesDir == "" { return environment } environment = setEnv(environment, "RUNFILES_DIR", runfilesDir) environment = setEnv(environment, "RUNFILES", runfilesDir) return environment } func setEnv(environment []string, key string, value string) []string { prefix := key + "=" for index, entry := range environment { if strings.HasPrefix(entry, prefix) { environment[index] = prefix + value return environment } } return append(environment, prefix+value) } func waitForURL(url string, timeout time.Duration, waitCh <-chan error) error { client := http.Client{Timeout: 2 * time.Second} deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { select { case err := <-waitCh: if err == nil { return fmt.Errorf("frontend dev server exited before becoming ready") } return fmt.Errorf("frontend dev server exited before becoming ready: %w", err) default: } response, err := client.Get(url) if err == nil { response.Body.Close() if response.StatusCode >= 200 && response.StatusCode < 400 { return nil } } time.Sleep(250 * time.Millisecond) } return fmt.Errorf("frontend dev server did not become ready at %s", url) } func resolveWatchCommand(target string) (*exec.Cmd, error) { for _, candidate := range []string{"ibazel", "bazelisk", "bazel"} { if _, err := exec.LookPath(candidate); err == nil { return exec.Command(candidate, "run", target), nil } } return nil, fmt.Errorf("neither ibazel, bazelisk, nor bazel is available in PATH") } func terminate(command *exec.Cmd, waitCh <-chan error) { if command.Process == nil { return } _ = command.Process.Kill() select { case <-waitCh: case <-time.After(2 * time.Second): } } func must(err error) { if err != nil { panic(err) } } func require(condition bool, message string) { if !condition { panic(message) } }