From 39431a101d61760de562643565eb20f3c8a4d5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 8 Jan 2021 22:30:21 +0100 Subject: [PATCH] Port select portions of Makefile to `script/build.go` This is to enable build tasks on Windows. --- .gitignore | 1 + Makefile | 42 ++++------- docs/source.md | 8 +- script/build.go | 196 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+), 32 deletions(-) create mode 100644 script/build.go diff --git a/.gitignore b/.gitignore index 9895939a6..905701534 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /site .github/**/node_modules /CHANGELOG.md +/script/build # VS Code .vscode diff --git a/Makefile b/Makefile index e29d67b07..85a46410b 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,3 @@ -BUILD_FILES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\ -{{end}}' ./...) - -GH_VERSION ?= $(shell git describe --tags 2>/dev/null || git rev-parse --short HEAD) -DATE_FMT = +%Y-%m-%d -ifdef SOURCE_DATE_EPOCH - BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)") -else - BUILD_DATE ?= $(shell date "$(DATE_FMT)") -endif - CGO_CPPFLAGS ?= ${CPPFLAGS} export CGO_CPPFLAGS CGO_CFLAGS ?= ${CFLAGS} @@ -16,27 +5,29 @@ export CGO_CFLAGS CGO_LDFLAGS ?= $(filter -g -L% -l% -O%,${LDFLAGS}) export CGO_LDFLAGS -GO_LDFLAGS := -X github.com/cli/cli/internal/build.Version=$(GH_VERSION) $(GO_LDFLAGS) -GO_LDFLAGS := -X github.com/cli/cli/internal/build.Date=$(BUILD_DATE) $(GO_LDFLAGS) -ifdef GH_OAUTH_CLIENT_SECRET - GO_LDFLAGS := -X github.com/cli/cli/internal/authflow.oauthClientID=$(GH_OAUTH_CLIENT_ID) $(GO_LDFLAGS) - GO_LDFLAGS := -X github.com/cli/cli/internal/authflow.oauthClientSecret=$(GH_OAUTH_CLIENT_SECRET) $(GO_LDFLAGS) -endif +.PHONY: bin/gh +bin/gh: script/build + @script/build bin/gh -bin/gh: $(BUILD_FILES) - go build -trimpath -ldflags "${GO_LDFLAGS}" -o "$@" ./cmd/gh +script/build: script/build.go + go build -o script/build script/build.go -clean: - rm -rf ./bin ./share .PHONY: clean +clean: script/build + @script/build clean +.PHONY: manpages +manpages: script/build + @script/build manpages + +.PHONY: test test: go test ./... -.PHONY: test site: git clone https://github.com/github/cli.github.com.git "$@" +.PHONY: site-docs site-docs: site git -C site pull git -C site rm 'manual/gh*.md' 2>/dev/null || true @@ -44,8 +35,8 @@ site-docs: site rm -f site/manual/*.bak git -C site add 'manual/gh*.md' git -C site commit -m 'update help docs' || true -.PHONY: site-docs +.PHONY: site-bump site-bump: site-docs ifndef GITHUB_REF $(error GITHUB_REF is not set) @@ -53,11 +44,6 @@ endif sed -i.bak -E 's/(assign version = )".+"/\1"$(GITHUB_REF:refs/tags/v%=%)"/' site/index.html rm -f site/index.html.bak git -C site commit -m '$(GITHUB_REF:refs/tags/v%=%)' index.html -.PHONY: site-bump - -.PHONY: manpages -manpages: - go run ./cmd/gen-docs --man-page --doc-path ./share/man/man1/ DESTDIR := prefix := /usr/local diff --git a/docs/source.md b/docs/source.md index 5a1a420f6..28c9a2312 100644 --- a/docs/source.md +++ b/docs/source.md @@ -22,18 +22,18 @@ # installs to '/usr/local' by default; sudo may be required $ make install - # install to a different location + # or, install to a different location $ make install prefix=/path/to/gh ``` #### Windows ```sh - # build the binary - > go build -o gh.exe ./cmd/gh + # build the `bin\gh.exe` binary + > go run script/build.go ``` There is no install step available on Windows. 3. Run `gh version` to check if it worked. #### Windows - Run `.\gh version` to check if it worked. + Run `bin\gh version` to check if it worked. diff --git a/script/build.go b/script/build.go new file mode 100644 index 000000000..3b1a08ea5 --- /dev/null +++ b/script/build.go @@ -0,0 +1,196 @@ +// Build tasks for the GitHub CLI project. +// +// Usage: go run script/build.go [] +// +// 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() { + task := "bin/gh" + if runtime.GOOS == "windows" { + task = "bin\\gh.exe" + } + + if len(os.Args) > 1 { + task = os.Args[1] + } + + self = filepath.Base(os.Args[0]) + if self == "build" { + self = "build.go" + } + + 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 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:]...) + 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")) +}