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 }