The main build script for this project is `script/build.go` which
implements Makefile-like building of the `gh` binary and associated man
pages. Our Makefile defers to the Go script.
However, when setting GOOS, GOARCH, and other environment variables to
modify the target for the resulting binary, these environment variables
would affect the execution of `build.go` as well, which was unintended.
This tweaks our Makefile to reset variables like GOOS and GOARCH when
building the `build.go` script itself, ensuring that the built script
runs on the same platform, and adds the ability to pass environment
variables as arguments to `go run script/build.go`. This allows the
following usage on platforms without `make`:
go run script/build.go GOOS=linux
With this style of invocation, the GOOS setting does not actually affect
`go run` itself; just the `go build` that is executed in a child process.
218 lines
4.9 KiB
Go
218 lines
4.9 KiB
Go
// Build tasks for the GitHub CLI project.
|
|
//
|
|
// Usage: go run script/build.go [<tasks>...] [<env>...]
|
|
//
|
|
// Known tasks are:
|
|
//
|
|
// bin/gh:
|
|
// Builds the main executable.
|
|
// Supported environment variables:
|
|
// - GH_VERSION: determined from source by default
|
|
// - GH_OAUTH_CLIENT_ID
|
|
// - GH_OAUTH_CLIENT_SECRET
|
|
// - SOURCE_DATE_EPOCH: enables reproducible builds
|
|
// - GO_LDFLAGS
|
|
//
|
|
// manpages:
|
|
// Builds the man pages under `share/man/man1/`.
|
|
//
|
|
// clean:
|
|
// Deletes all built files.
|
|
//
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cli/safeexec"
|
|
)
|
|
|
|
var tasks = map[string]func(string) error{
|
|
"bin/gh": func(exe string) error {
|
|
info, err := os.Stat(exe)
|
|
if err == nil && !sourceFilesLaterThan(info.ModTime()) {
|
|
fmt.Printf("%s: `%s` is up to date.\n", self, exe)
|
|
return nil
|
|
}
|
|
|
|
ldflags := os.Getenv("GO_LDFLAGS")
|
|
ldflags = fmt.Sprintf("-X github.com/cli/cli/internal/build.Version=%s %s", version(), ldflags)
|
|
ldflags = fmt.Sprintf("-X github.com/cli/cli/internal/build.Date=%s %s", date(), ldflags)
|
|
if oauthSecret := os.Getenv("GH_OAUTH_CLIENT_SECRET"); oauthSecret != "" {
|
|
ldflags = fmt.Sprintf("-X github.com/cli/cli/internal/authflow.oauthClientSecret=%s %s", oauthSecret, ldflags)
|
|
ldflags = fmt.Sprintf("-X github.com/cli/cli/internal/authflow.oauthClientID=%s %s", os.Getenv("GH_OAUTH_CLIENT_ID"), ldflags)
|
|
}
|
|
|
|
return run("go", "build", "-trimpath", "-ldflags", ldflags, "-o", exe, "./cmd/gh")
|
|
},
|
|
"manpages": func(_ string) error {
|
|
return run("go", "run", "./cmd/gen-docs", "--man-page", "--doc-path", "./share/man/man1/")
|
|
},
|
|
"clean": func(_ string) error {
|
|
return rmrf("bin", "share")
|
|
},
|
|
}
|
|
|
|
var self string
|
|
|
|
func main() {
|
|
args := os.Args[:1]
|
|
for _, arg := range os.Args[1:] {
|
|
if idx := strings.IndexRune(arg, '='); idx >= 0 {
|
|
os.Setenv(arg[:idx], arg[idx+1:])
|
|
} else {
|
|
args = append(args, arg)
|
|
}
|
|
}
|
|
|
|
if len(args) < 2 {
|
|
if isWindowsTarget() {
|
|
args = append(args, filepath.Join("bin", "gh.exe"))
|
|
} else {
|
|
args = append(args, "bin/gh")
|
|
}
|
|
}
|
|
|
|
self = filepath.Base(args[0])
|
|
if self == "build" {
|
|
self = "build.go"
|
|
}
|
|
|
|
for _, task := range args[1:] {
|
|
t := tasks[normalizeTask(task)]
|
|
if t == nil {
|
|
fmt.Fprintf(os.Stderr, "Don't know how to build task `%s`.\n", task)
|
|
os.Exit(1)
|
|
}
|
|
|
|
err := t(task)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
fmt.Fprintf(os.Stderr, "%s: building task `%s` failed.\n", self, task)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func isWindowsTarget() bool {
|
|
if os.Getenv("GOOS") == "windows" {
|
|
return true
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func version() string {
|
|
if versionEnv := os.Getenv("GH_VERSION"); versionEnv != "" {
|
|
return versionEnv
|
|
}
|
|
if desc, err := cmdOutput("git", "describe", "--tags"); err == nil {
|
|
return desc
|
|
}
|
|
rev, _ := cmdOutput("git", "rev-parse", "--short", "HEAD")
|
|
return rev
|
|
}
|
|
|
|
func date() string {
|
|
t := time.Now()
|
|
if sourceDate := os.Getenv("SOURCE_DATE_EPOCH"); sourceDate != "" {
|
|
if sec, err := strconv.ParseInt(sourceDate, 10, 64); err == nil {
|
|
t = time.Unix(sec, 0)
|
|
}
|
|
}
|
|
return t.Format("2006-01-02")
|
|
}
|
|
|
|
func sourceFilesLaterThan(t time.Time) bool {
|
|
foundLater := false
|
|
_ = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if foundLater {
|
|
return filepath.SkipDir
|
|
}
|
|
if len(path) > 1 && (path[0] == '.' || path[0] == '_') {
|
|
if info.IsDir() {
|
|
return filepath.SkipDir
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
if path == "go.mod" || path == "go.sum" || (strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go")) {
|
|
if info.ModTime().After(t) {
|
|
foundLater = true
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return foundLater
|
|
}
|
|
|
|
func rmrf(targets ...string) error {
|
|
args := append([]string{"rm", "-rf"}, targets...)
|
|
announce(args...)
|
|
for _, target := range targets {
|
|
if err := os.RemoveAll(target); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func announce(args ...string) {
|
|
fmt.Println(shellInspect(args))
|
|
}
|
|
|
|
func run(args ...string) error {
|
|
exe, err := safeexec.LookPath(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
announce(args...)
|
|
cmd := exec.Command(exe, args[1:]...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
func cmdOutput(args ...string) (string, error) {
|
|
exe, err := safeexec.LookPath(args[0])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cmd := exec.Command(exe, args[1:]...)
|
|
cmd.Stderr = ioutil.Discard
|
|
out, err := cmd.Output()
|
|
return strings.TrimSuffix(string(out), "\n"), err
|
|
}
|
|
|
|
func shellInspect(args []string) string {
|
|
fmtArgs := make([]string, len(args))
|
|
for i, arg := range args {
|
|
if strings.ContainsAny(arg, " \t'\"") {
|
|
fmtArgs[i] = fmt.Sprintf("%q", arg)
|
|
} else {
|
|
fmtArgs[i] = arg
|
|
}
|
|
}
|
|
return strings.Join(fmtArgs, " ")
|
|
}
|
|
|
|
func normalizeTask(t string) string {
|
|
return filepath.ToSlash(strings.TrimSuffix(t, ".exe"))
|
|
}
|