From c4896ed642d97d78c0a47a9761d4ede5d586b54f Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 11 Oct 2018 22:36:23 +0200 Subject: [PATCH] Add build-release-binaries --- helpers/.gitignore | 2 + helpers/build-release-binaries/main.go | 299 +++++++++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 helpers/.gitignore create mode 100644 helpers/build-release-binaries/main.go diff --git a/helpers/.gitignore b/helpers/.gitignore new file mode 100644 index 000000000..299320af1 --- /dev/null +++ b/helpers/.gitignore @@ -0,0 +1,2 @@ +build-release-binaries/build-release-binaries +prepare-release/prepare-release diff --git a/helpers/build-release-binaries/main.go b/helpers/build-release-binaries/main.go new file mode 100644 index 000000000..f12fc8ddd --- /dev/null +++ b/helpers/build-release-binaries/main.go @@ -0,0 +1,299 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strings" + "time" + + "github.com/spf13/pflag" + "golang.org/x/sync/errgroup" +) + +var opts = struct { + Verbose bool + SourceDir string + OutputDir string +}{} + +func init() { + pflag.BoolVarP(&opts.Verbose, "verbose", "v", false, "be verbose") + pflag.StringVarP(&opts.SourceDir, "source", "s", "/restic", "path to the source code `directory`") + pflag.StringVarP(&opts.OutputDir, "output", "o", "/output", "path to the output `directory`") + pflag.Parse() +} + +func die(f string, args ...interface{}) { + if !strings.HasSuffix(f, "\n") { + f += "\n" + } + f = "\x1b[31m" + f + "\x1b[0m" + fmt.Fprintf(os.Stderr, f, args...) + os.Exit(1) +} + +func msg(f string, args ...interface{}) { + if !strings.HasSuffix(f, "\n") { + f += "\n" + } + f = "\x1b[32m" + f + "\x1b[0m" + fmt.Printf(f, args...) +} + +func verbose(f string, args ...interface{}) { + if !opts.Verbose { + return + } + if !strings.HasSuffix(f, "\n") { + f += "\n" + } + f = "\x1b[32m" + f + "\x1b[0m" + fmt.Printf(f, args...) +} + +func run(cmd string, args ...string) { + c := exec.Command(cmd, args...) + c.Stdout = os.Stdout + c.Stderr = os.Stderr + err := c.Run() + if err != nil { + die("error running %s %s: %v", cmd, args, err) + } +} + +func rm(file string) { + err := os.Remove(file) + + if os.IsNotExist(err) { + err = nil + } + + if err != nil { + die("error removing %v: %v", file, err) + } +} + +func rmdir(dir string) { + err := os.RemoveAll(dir) + if err != nil { + die("error removing %v: %v", dir, err) + } +} + +func mkdir(dir string) { + err := os.MkdirAll(dir, 0755) + if err != nil { + die("mkdir %v: %v", dir, err) + } +} + +func getwd() string { + pwd, err := os.Getwd() + if err != nil { + die("Getwd(): %v", err) + } + return pwd +} + +func abs(dir string) string { + absDir, err := filepath.Abs(dir) + if err != nil { + die("unable to find absolute path for %v: %v", dir, err) + } + return absDir +} + +func build(sourceDir, outputDir, goos, goarch string) (filename string) { + filename = fmt.Sprintf("%v_%v_%v", "restic", goos, goarch) + if goos == "windows" { + filename += ".exe" + } + outputFile := filepath.Join(outputDir, filename) + + c := exec.Command("go", "build", + "-mod=vendor", + "-o", outputFile, + "-ldflags", "-s -w", + "-tags", "selfupdate", + "./cmd/restic", + ) + c.Stdout = os.Stdout + c.Stderr = os.Stderr + c.Dir = sourceDir + + verbose("run %v %v in %v", "go", c.Args, c.Dir) + + c.Dir = sourceDir + c.Env = append(os.Environ(), + "CGO_ENABLED=0", + "GOPROXY=off", + "GOOS="+goos, + "GOARCH="+goarch, + ) + + err := c.Run() + if err != nil { + die("error building %v/%v: %v", goos, goarch, err) + } + + return filename +} + +func modTime(file string) time.Time { + fi, err := os.Lstat(file) + if err != nil { + die("unable to get modtime of %v: %v", file, err) + } + + return fi.ModTime() +} + +func touch(file string, t time.Time) { + err := os.Chtimes(file, t, t) + if err != nil { + die("unable to update timestamps for %v: %v", file, err) + } +} + +func chmod(file string, mode os.FileMode) { + err := os.Chmod(file, mode) + if err != nil { + die("unable to chmod %v to %s: %v", file, mode, err) + } +} + +func compress(goos, inputDir, filename string) (outputFile string) { + var c *exec.Cmd + switch goos { + case "windows": + outputFile = strings.TrimSuffix(filename, ".exe") + ".zip" + c = exec.Command("zip", "-q", "-X", outputFile, filename) + c.Dir = inputDir + default: + outputFile = filename + ".bz2" + c = exec.Command("bzip2", filename) + c.Dir = inputDir + } + + rm(filepath.Join(inputDir, outputFile)) + + c.Stdout = os.Stdout + c.Stderr = os.Stderr + c.Dir = inputDir + + verbose("run %v %v in %v", "go", c.Args, c.Dir) + + err := c.Run() + if err != nil { + die("error compressing: %v", err) + } + + rm(filepath.Join(inputDir, filename)) + + return outputFile +} + +func buildForTarget(sourceDir, outputDir, goos, goarch string) (filename string) { + mtime := modTime(filepath.Join(sourceDir, "VERSION")) + + filename = build(sourceDir, outputDir, goos, goarch) + touch(filepath.Join(outputDir, filename), mtime) + chmod(filepath.Join(outputDir, filename), 0755) + filename = compress(goos, outputDir, filename) + return filename +} + +func sha256sums(outputFile, inputDir string, filenames []string) { + verbose("runnnig sha256sum in %v", inputDir) + f, err := os.Create(outputFile) + if err != nil { + die("unable to create %v: %v", outputFile, err) + } + + c := exec.Command("sha256sum", filenames...) + c.Stdout = f + c.Stderr = os.Stderr + c.Dir = inputDir + + err = c.Run() + if err != nil { + die("error running sha256sums: %v", err) + } + + err = f.Close() + if err != nil { + die("close %v: %v", outputFile, err) + } +} + +func buildTargets(sourceDir, outputDir string, targets map[string][]string) { + msg("building with %d workers", runtime.NumCPU()) + + type Job struct{ GOOS, GOARCH string } + + var wg errgroup.Group + ch := make(chan Job) + res := make(chan string) + + for i := 0; i < runtime.NumCPU(); i++ { + wg.Go(func() error { + for job := range ch { + start := time.Now() + verbose("build %v/%v", job.GOOS, job.GOARCH) + file := buildForTarget(sourceDir, outputDir, job.GOOS, job.GOARCH) + msg("built %v/%v in %.3fs", job.GOOS, job.GOARCH, time.Since(start).Seconds()) + res <- file + } + return nil + }) + } + + wg.Go(func() error { + for goos, archs := range targets { + for _, goarch := range archs { + ch <- Job{goos, goarch} + } + } + close(ch) + return nil + }) + + go func() { + _ = wg.Wait() + close(res) + }() + + start := time.Now() + var filenames []string + for filename := range res { + filenames = append(filenames, filename) + } + sort.Strings(filenames) + + msg("build finished in %.3fs, create SHA256SUMS", time.Since(start).Seconds()) + sha256sums(filepath.Join(outputDir, "SHA256SUMS"), outputDir, filenames) +} + +var defaultBuildTargets = map[string][]string{ + "darwin": []string{"386", "amd64"}, + "freebsd": []string{"386", "amd64", "arm"}, + "linux": []string{"386", "amd64", "arm", "arm64"}, + "openbsd": []string{"386", "amd64"}, + "windows": []string{"386", "amd64"}, +} + +func main() { + if len(pflag.Args()) != 0 { + die("USAGE: build-release-binaries [OPTIONS]") + } + + sourceDir := abs(opts.SourceDir) + outputDir := abs(opts.OutputDir) + mkdir(outputDir) + + buildTargets(sourceDir, outputDir, defaultBuildTargets) +}