github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/script/build.go (about)

     1  // Build tasks for the GitHub CLI project.
     2  //
     3  // Usage:  go run script/build.go [<tasks>...] [<env>...]
     4  //
     5  // Known tasks are:
     6  //
     7  //   bin/gh:
     8  //     Builds the main executable.
     9  //     Supported environment variables:
    10  //     - GH_VERSION: determined from source by default
    11  //     - GH_OAUTH_CLIENT_ID
    12  //     - GH_OAUTH_CLIENT_SECRET
    13  //     - SOURCE_DATE_EPOCH: enables reproducible builds
    14  //     - GO_LDFLAGS
    15  //
    16  //   manpages:
    17  //     Builds the man pages under `share/man/man1/`.
    18  //
    19  //   clean:
    20  //     Deletes all built files.
    21  //
    22  
    23  package main
    24  
    25  import (
    26  	"fmt"
    27  	"io/ioutil"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"runtime"
    32  	"strconv"
    33  	"strings"
    34  	"time"
    35  
    36  	"github.com/cli/safeexec"
    37  )
    38  
    39  var tasks = map[string]func(string) error{
    40  	"bin/gh": func(exe string) error {
    41  		info, err := os.Stat(exe)
    42  		if err == nil && !sourceFilesLaterThan(info.ModTime()) {
    43  			fmt.Printf("%s: `%s` is up to date.\n", self, exe)
    44  			return nil
    45  		}
    46  
    47  		ldflags := os.Getenv("GO_LDFLAGS")
    48  		ldflags = fmt.Sprintf("-X github.com/abdfnx/gh-api/internal/build.Version=%s %s", version(), ldflags)
    49  		ldflags = fmt.Sprintf("-X github.com/abdfnx/gh-api/internal/build.Date=%s %s", date(), ldflags)
    50  		if oauthSecret := os.Getenv("GH_OAUTH_CLIENT_SECRET"); oauthSecret != "" {
    51  			ldflags = fmt.Sprintf("-X github.com/abdfnx/gh-api/internal/authflow.oauthClientSecret=%s %s", oauthSecret, ldflags)
    52  			ldflags = fmt.Sprintf("-X github.com/abdfnx/gh-api/internal/authflow.oauthClientID=%s %s", os.Getenv("GH_OAUTH_CLIENT_ID"), ldflags)
    53  		}
    54  
    55  		return run("go", "build", "-trimpath", "-ldflags", ldflags, "-o", exe, "./cmd/gh")
    56  	},
    57  	"manpages": func(_ string) error {
    58  		return run("go", "run", "./cmd/gen-docs", "--man-page", "--doc-path", "./share/man/man1/")
    59  	},
    60  	"clean": func(_ string) error {
    61  		return rmrf("bin", "share")
    62  	},
    63  }
    64  
    65  var self string
    66  
    67  func main() {
    68  	args := os.Args[:1]
    69  	for _, arg := range os.Args[1:] {
    70  		if idx := strings.IndexRune(arg, '='); idx >= 0 {
    71  			os.Setenv(arg[:idx], arg[idx+1:])
    72  		} else {
    73  			args = append(args, arg)
    74  		}
    75  	}
    76  
    77  	if len(args) < 2 {
    78  		if isWindowsTarget() {
    79  			args = append(args, filepath.Join("bin", "gh.exe"))
    80  		} else {
    81  			args = append(args, "bin/gh")
    82  		}
    83  	}
    84  
    85  	self = filepath.Base(args[0])
    86  	if self == "build" {
    87  		self = "build.go"
    88  	}
    89  
    90  	for _, task := range args[1:] {
    91  		t := tasks[normalizeTask(task)]
    92  		if t == nil {
    93  			fmt.Fprintf(os.Stderr, "Don't know how to build task `%s`.\n", task)
    94  			os.Exit(1)
    95  		}
    96  
    97  		err := t(task)
    98  		if err != nil {
    99  			fmt.Fprintln(os.Stderr, err)
   100  			fmt.Fprintf(os.Stderr, "%s: building task `%s` failed.\n", self, task)
   101  			os.Exit(1)
   102  		}
   103  	}
   104  }
   105  
   106  func isWindowsTarget() bool {
   107  	if os.Getenv("GOOS") == "windows" {
   108  		return true
   109  	}
   110  	if runtime.GOOS == "windows" {
   111  		return true
   112  	}
   113  	return false
   114  }
   115  
   116  func version() string {
   117  	if versionEnv := os.Getenv("GH_VERSION"); versionEnv != "" {
   118  		return versionEnv
   119  	}
   120  	if desc, err := cmdOutput("git", "describe", "--tags"); err == nil {
   121  		return desc
   122  	}
   123  	rev, _ := cmdOutput("git", "rev-parse", "--short", "HEAD")
   124  	return rev
   125  }
   126  
   127  func date() string {
   128  	t := time.Now()
   129  	if sourceDate := os.Getenv("SOURCE_DATE_EPOCH"); sourceDate != "" {
   130  		if sec, err := strconv.ParseInt(sourceDate, 10, 64); err == nil {
   131  			t = time.Unix(sec, 0)
   132  		}
   133  	}
   134  	return t.Format("2006-01-02")
   135  }
   136  
   137  func sourceFilesLaterThan(t time.Time) bool {
   138  	foundLater := false
   139  	_ = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
   140  		if err != nil {
   141  			return err
   142  		}
   143  		if foundLater {
   144  			return filepath.SkipDir
   145  		}
   146  		if len(path) > 1 && (path[0] == '.' || path[0] == '_') {
   147  			if info.IsDir() {
   148  				return filepath.SkipDir
   149  			} else {
   150  				return nil
   151  			}
   152  		}
   153  		if info.IsDir() {
   154  			return nil
   155  		}
   156  		if path == "go.mod" || path == "go.sum" || (strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go")) {
   157  			if info.ModTime().After(t) {
   158  				foundLater = true
   159  			}
   160  		}
   161  		return nil
   162  	})
   163  	return foundLater
   164  }
   165  
   166  func rmrf(targets ...string) error {
   167  	args := append([]string{"rm", "-rf"}, targets...)
   168  	announce(args...)
   169  	for _, target := range targets {
   170  		if err := os.RemoveAll(target); err != nil {
   171  			return err
   172  		}
   173  	}
   174  	return nil
   175  }
   176  
   177  func announce(args ...string) {
   178  	fmt.Println(shellInspect(args))
   179  }
   180  
   181  func run(args ...string) error {
   182  	exe, err := safeexec.LookPath(args[0])
   183  	if err != nil {
   184  		return err
   185  	}
   186  	announce(args...)
   187  	cmd := exec.Command(exe, args[1:]...)
   188  	cmd.Stdout = os.Stdout
   189  	cmd.Stderr = os.Stderr
   190  	return cmd.Run()
   191  }
   192  
   193  func cmdOutput(args ...string) (string, error) {
   194  	exe, err := safeexec.LookPath(args[0])
   195  	if err != nil {
   196  		return "", err
   197  	}
   198  	cmd := exec.Command(exe, args[1:]...)
   199  	cmd.Stderr = ioutil.Discard
   200  	out, err := cmd.Output()
   201  	return strings.TrimSuffix(string(out), "\n"), err
   202  }
   203  
   204  func shellInspect(args []string) string {
   205  	fmtArgs := make([]string, len(args))
   206  	for i, arg := range args {
   207  		if strings.ContainsAny(arg, " \t'\"") {
   208  			fmtArgs[i] = fmt.Sprintf("%q", arg)
   209  		} else {
   210  			fmtArgs[i] = arg
   211  		}
   212  	}
   213  	return strings.Join(fmtArgs, " ")
   214  }
   215  
   216  func normalizeTask(t string) string {
   217  	return filepath.ToSlash(strings.TrimSuffix(t, ".exe"))
   218  }