Thu, 18 Dec 2025 19:03:53 +0100
wrap build commands in separate shell scripts
package main import ( "bytes" "encoding/json" "encoding/xml" "fmt" "log" "os" "os/exec" "path" "strconv" "strings" "text/template" ) type Config struct { XMLName xml.Name `xml:"config"` BuildEnv []BuildEnv `xml:"buildenv"` BuildEnvMap map[string]BuildEnv Repository []Repository `xml:"repository"` } type BuildEnv struct { Name string `xml:"name,attr"` Host string `xml:"host"` User string `xml:"user"` } type Build struct { Name string `xml:"name,attr"` IsPlatform string `xml:"isplatform,attr"` NotPlatform string `xml:"not,attr"` NotList []string Test []string `xml:"test"` Var []struct { Name string `xml:"name,attr"` Value string `xml:",chardata"` } Configure string `xml:"configure"` Compile string `xml:"compile"` Check string `xml:"check"` } type Repository struct { Path string `xml:"path"` BuildEnvs string `xml:"buildenvs"` Build []Build `xml:"build"` } type Result struct { Build *Build Id string `json:"build"` Name string `json:"name"` Configure string `json:"configure"` Compile string `json:"compile"` Check string `json:"check"` } type BuildEnvResult struct { Env *BuildEnv Results []Result } type Commit struct { Rev string NodeShort string Node string Desc string Author string Date string } type CommitResult struct { Commit *Commit Results []BuildEnvResult } func main() { data, err := os.ReadFile("testconfig.xml") if err != nil { log.Fatal(err) } config := &Config{} if err := xml.Unmarshal(data, &config); err != nil { log.Fatal(err) } config.BuildEnvMap = make(map[string]BuildEnv) for _, env := range config.BuildEnv { config.BuildEnvMap[env.Name] = env } for ri := range config.Repository { for bi := range config.Repository[ri].Build { build := &config.Repository[ri].Build[bi] build.NotList = strings.Split(build.NotPlatform, ",") } } templateStr, err := os.ReadFile("build.template") if err != nil { log.Fatal(err) } funcs := template.FuncMap{ "add": func(i int, n int) int { return i + n }, "sub": func(i int, n int) int { return i - n }, } tpl, err := template.New("").Funcs(funcs).Parse(string(templateStr)) if err != nil { log.Fatal(err) } err = os.Mkdir("tmp", 0755) if err != nil && !os.IsExist(err) { log.Fatal(err) } tmp := "tmp" out := "out" for _, repo := range config.Repository { commit := get_commit_info(repo.Path) if commit == nil { log.Print("cannot get current commit info for repo ", repo.Path) continue } result_dir := path.Join(out, commit.Node) _, err := os.Stat(result_dir) if err == nil { log.Printf("repository %s commit %s already built", repo.Path, commit.NodeShort) continue } if err := create_repo_archive(&repo, tpl, tmp); err != nil { log.Fatal(err) } results := exec_buildenvs(config, &repo, tmp) err = save_result(&repo, commit, results, tmp) if err != nil { log.Print("cannot save results for commit ", commit.NodeShort) } } } func create_repo_archive(repo *Repository, tpl *template.Template, tmp_dir string) error { // create a build directory, that contains the repository source // and build scripts buildPath := path.Join(tmp_dir, "build") if err := os.RemoveAll(buildPath); err != nil { log.Print("remove build dir failed: ", err) return err } if err := os.RemoveAll(path.Join(tmp_dir, "build.tar")); err != nil { log.Print("remove build.tar failed") return err } if err := os.RemoveAll(path.Join(tmp_dir, "build.tar.gz")); err != nil { log.Print("remove build.tar.gz failed") return err } if err := os.RemoveAll(path.Join(tmp_dir, "result")); err != nil { log.Print("remove result failed") return err } err := os.Mkdir(buildPath, 0755) if err != nil { log.Print("mkdir build dir failed: ", err) return err } // update repository and copy it to the build directory cmd := exec.Command("hg", "pull") var outb, errb bytes.Buffer cmd.Dir = repo.Path cmd.Stdout = &outb cmd.Stderr = &errb err = cmd.Run() if err != nil { log.Print("hg: ", errb.String()) return err } outb.Reset() errb.Reset() cmd = exec.Command("hg", "update") cmd.Dir = repo.Path cmd.Stdout = &outb cmd.Stderr = &errb err = cmd.Run() if err != nil { log.Print("hg: ", errb.String()) return err } // copy repository to the tmp_dir build directory cmd = exec.Command("cp", "-r", repo.Path, path.Join(buildPath, "src")) err = cmd.Run() if err != nil { log.Print("cannot copy the repository to the build dir") return err } // create build script file, err := os.Create(path.Join(buildPath, "build.sh")) if err != nil { log.Print("cannot create build.sh") return err } defer file.Close() err = tpl.Execute(file, repo) if err != nil { log.Print("cannot execute build.sh template") return err } file.Chmod(0755) // create tarball cmd = exec.Command("tar", "cvf", "../build.tar", "src", "build.sh") cmd.Dir = buildPath if err := cmd.Run(); err != nil { log.Print("tar error") return err } cmd = exec.Command("gzip", path.Join(tmp_dir, "build.tar")) if err := cmd.Run(); err != nil { log.Print("gzip error") return err } return nil } func host_is_available(host string) bool { cmd := exec.Command("ping", "-c", "1", host) if err := cmd.Run(); err != nil { return false } return true } func get_build(builds []Build, id string) *Build { i, err := strconv.Atoi(id) if err != nil { return nil } if i < 0 || i >= len(builds) { return nil } return &builds[i] } func exec_buildenvs(config *Config, repo *Repository, tmp_dir string) []BuildEnvResult { var buildResults []BuildEnvResult if len(repo.BuildEnvs) == 0 { log.Print("repo %s has no buildenvs", repo.Path) return buildResults } buildenvs := strings.Split(repo.BuildEnvs, ",") for _, buildenv := range buildenvs { env, ok := config.BuildEnvMap[buildenv] if !ok { log.Print("unknown build env ", buildenv) continue } if !host_is_available(env.Host) { log.Print("skip unavailable host ", env.Host) continue } // steps: // upload build.tar.gz // extract build.tar.gz on the buildenv host and run build.sh // upload using scp cmd := exec.Command("scp", path.Join(tmp_dir, "build.tar.gz"), env.Host+":") log.Print("scp: ", cmd.Args) if err := cmd.Run(); err != nil { log.Print("cannot copy build env to ", env.Host) continue } // run build.sh build_dir := "build" remote_command := fmt.Sprintf("sh -c 'rm -Rf %[1]s; mkdir -p %[1]s; gzip -dc build.tar.gz | tar xf - -C %[1]s ; (cd %[1]s; ./build.sh)'", build_dir) log.Printf("host: %s: command: %s", env.Host, remote_command) var outb, errb bytes.Buffer cmd = exec.Command("ssh", "-t", env.Host, remote_command) cmd.Stdout = &outb cmd.Stderr = &errb err := cmd.Run() log.Printf("host: %s: stdout: %s", env.Host, outb.String()) if err != nil { log.Print("cannot execute build.sh on buildenv host ", env.Host) log.Print("stderr: ", errb.String()) continue } // download result cmd.Stdout = nil cmd.Stderr = nil cmd = exec.Command("scp", "-r", env.Host+":"+build_dir+"/result", tmp_dir) log.Print("scp: ", cmd.Args) if err := cmd.Run(); err != nil { log.Print("cannot get result from buildenv") continue } // parse result resultData, err := os.ReadFile(path.Join(tmp_dir, "result/result.json")) if err != nil { log.Print("cannot read result from buildenv") continue } var results []Result if err := json.Unmarshal(resultData, &results); err != nil { log.Print("cannot parse result.json") continue } // store the actual Build pointer in the result for i := range results { results[i].Build = get_build(repo.Build, results[i].Id) } log.Print("buildenv finished ", env.Name) envResult := BuildEnvResult{Env: &env, Results: results} buildResults = append(buildResults, envResult) } return buildResults } func get_commit_info(repo_dir string) *Commit { var outb, errb bytes.Buffer commit := &Commit{} cmd := exec.Command("hg", "log", "-r", ".", "-T", "{rev}\\t{node|short}\\t{node}\\t{date|isodate}") cmd.Dir = repo_dir cmd.Stdout = &outb cmd.Stderr = &errb if err := cmd.Run(); err != nil { log.Print(outb.String()) log.Print(errb.String()) return nil } info := strings.Split(outb.String(), "\t") if len(info) != 4 { log.Print(outb.String()) log.Print(errb.String()) return nil } commit.Rev = info[0] commit.NodeShort = info[1] commit.Node = info[2] commit.Date = info[3] outb.Reset() errb.Reset() cmd = exec.Command("hg", "log", "-r", ".", "-T", "{author}") cmd.Dir = repo_dir cmd.Stdout = &outb cmd.Stderr = &errb if err := cmd.Run(); err != nil { log.Print(outb.String()) log.Print(errb.String()) return nil } commit.Author = outb.String() cmd = exec.Command("hg", "log", "-r", ".", "-T", "{desc}") cmd.Dir = repo_dir cmd.Stdout = &outb cmd.Stderr = &errb if err := cmd.Run(); err != nil { log.Print(outb.String()) log.Print(errb.String()) return nil } commit.Desc = outb.String() return commit } func save_result(repo *Repository, commit *Commit, result []BuildEnvResult, tmp_dir string) error { out_dir := "out/" src := path.Join(tmp_dir, "result") dst := path.Join(out_dir, commit.Node) // move result if err := os.Rename(src, dst); err != nil { return err } return nil }