package main import ( "bytes" "flag" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "time" ) var ( key = "" repo = "" commit = "" task = "" image = "" port = "" ) func init() { flag.StringVar(&key, "key", "", "Path to SSH private key") flag.StringVar(&repo, "repo", "", "Source code as valid Git URL") flag.StringVar(&commit, "commit", "", "Commit or commit-ish reference for checkout") flag.StringVar(&task, "task", "", "Shell code to execute for the build") flag.StringVar(&image, "image", "", "QEMU qcow2 image which contains the VM") flag.StringVar(&port, "port", "", "localhost port which is connected to VM SSH server") } func main() { flag.Parse() log.SetFlags(0) log.Println("Starting craft") defer log.Println("Exiting craft") vm := qemu() err := vm.Start() if err != nil { log.Fatal(err) } defer func() { err := vm.Process.Kill() if err != nil { log.Fatal(err) } }() waitBoot() workbench := prepareWorkbench(repo, commit) defer os.RemoveAll(workbench) craft(workbench) } func runCommand(dir string, name string, args ...string) { log.Printf("%s %s\n", name, strings.Join(args, " ")) command := exec.Command(name, args...) command.Dir = dir command.Stderr = os.Stderr command.Stdout = os.Stdout err := command.Run() if err != nil { log.Fatal(err) } } func qemu() *exec.Cmd { return exec.Command( "qemu-system-x86_64", "-enable-kvm", "-m", "4G", "-net", "nic,model=virtio", "-net", fmt.Sprintf( "user,hostfwd=tcp:127.0.0.1:%s-:22", port, ), "-drive", fmt.Sprintf( "file=%s,media=disk,snapshot=on,if=virtio", image, ), "-smp", "cpus=4", "-nographic", ) } func sshCommand(name string, arg ...string) *exec.Cmd { return exec.Command( "ssh", append([]string{ "-p", port, "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-i", key, "root@localhost", name, }, arg...)..., ) } func waitBoot() { retries := 5 initialDelay := 1 * time.Second log.Println("Waiting for VM to boot") time.Sleep(initialDelay) // without that `ssh` returns too quickly for range retries { cmd := sshCommand("true") err := cmd.Run() if err == nil { log.Println("VM booted") return } } log.Fatalf("Could not reach VM %d times - giving up", retries) } func craft(workbench string) { script := fmt.Sprintf(`#!/bin/sh %s `, task) cmd := exec.Command("sh") cmd.Dir = workbench cmd.Stdin = bytes.NewBufferString(script) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { log.Fatal(err) } } func prepareWorkbench(repo string, commit string) string { workbench, err := os.MkdirTemp("", "*-craft") if err != nil { log.Fatal(err) } runCommand( workbench, "git", "clone", repo, workbench, ) runCommand( workbench, "git", "--git-dir", filepath.Join(workbench, ".git"), "--work-tree", workbench, "checkout", commit, ) runCommand( workbench, "git", "--git-dir", filepath.Join(workbench, ".git"), "--work-tree", workbench, "submodule", "update", "--depth=1", "--init", "--recursive", ) return workbench }