package builder import ( "bytes" "context" "encoding/json" "fmt" "os/exec" "time" ) // Executor handles the execution of nix build commands. type Executor struct { timeout time.Duration } // NewExecutor creates a new build executor. func NewExecutor(timeout time.Duration) *Executor { return &Executor{ timeout: timeout, } } // BuildResult contains the result of a build execution. type BuildResult struct { Success bool ExitCode int Stdout string Stderr string Error error } // FlakeShowResult contains the parsed output of nix flake show. type FlakeShowResult struct { NixosConfigurations map[string]any `json:"nixosConfigurations"` } // ListHosts returns the list of hosts (nixosConfigurations) available in a flake. func (e *Executor) ListHosts(ctx context.Context, flakeURL, branch string) ([]string, error) { ctx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() flakeRef := fmt.Sprintf("%s?ref=%s", flakeURL, branch) cmd := exec.CommandContext(ctx, "nix", "flake", "show", "--json", flakeRef) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { if ctx.Err() == context.DeadlineExceeded { return nil, fmt.Errorf("timeout listing hosts") } return nil, fmt.Errorf("failed to list hosts: %w\n%s", err, stderr.String()) } var result FlakeShowResult if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { return nil, fmt.Errorf("failed to parse flake show output: %w", err) } hosts := make([]string, 0, len(result.NixosConfigurations)) for host := range result.NixosConfigurations { hosts = append(hosts, host) } return hosts, nil } // Build builds a single host's system configuration. func (e *Executor) Build(ctx context.Context, flakeURL, branch, host string) *BuildResult { ctx, cancel := context.WithTimeout(ctx, e.timeout) defer cancel() // Build the flake reference for the system toplevel flakeRef := fmt.Sprintf("%s?ref=%s#nixosConfigurations.%s.config.system.build.toplevel", flakeURL, branch, host) cmd := exec.CommandContext(ctx, "nix", "build", "--no-link", flakeRef) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() result := &BuildResult{ Stdout: stdout.String(), Stderr: stderr.String(), } if err != nil { result.Success = false result.Error = err if ctx.Err() == context.DeadlineExceeded { result.Error = fmt.Errorf("build timed out after %v", e.timeout) } if exitErr, ok := err.(*exec.ExitError); ok { result.ExitCode = exitErr.ExitCode() } else { result.ExitCode = -1 } } else { result.Success = true result.ExitCode = 0 } return result } // BuildCommand returns the command that would be executed (for logging/debugging). func (e *Executor) BuildCommand(flakeURL, branch, host string) string { flakeRef := fmt.Sprintf("%s?ref=%s#nixosConfigurations.%s.config.system.build.toplevel", flakeURL, branch, host) return fmt.Sprintf("nix build --no-link %s", flakeRef) }