github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/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 "errors" 27 "fmt" 28 "io" 29 "os" 30 "os/exec" 31 "path/filepath" 32 "runtime" 33 "strconv" 34 "strings" 35 "time" 36 37 "github.com/cli/safeexec" 38 ) 39 40 var tasks = map[string]func(string) error{ 41 "bin/gh": func(exe string) error { 42 info, err := os.Stat(exe) 43 if err == nil && !sourceFilesLaterThan(info.ModTime()) { 44 fmt.Printf("%s: `%s` is up to date.\n", self, exe) 45 return nil 46 } 47 48 ldflags := os.Getenv("GO_LDFLAGS") 49 ldflags = fmt.Sprintf("-X github.com/ungtb10d/cli/v2/internal/build.Version=%s %s", version(), ldflags) 50 ldflags = fmt.Sprintf("-X github.com/ungtb10d/cli/v2/internal/build.Date=%s %s", date(), ldflags) 51 if oauthSecret := os.Getenv("GH_OAUTH_CLIENT_SECRET"); oauthSecret != "" { 52 ldflags = fmt.Sprintf("-X github.com/ungtb10d/cli/v2/internal/authflow.oauthClientSecret=%s %s", oauthSecret, ldflags) 53 ldflags = fmt.Sprintf("-X github.com/ungtb10d/cli/v2/internal/authflow.oauthClientID=%s %s", os.Getenv("GH_OAUTH_CLIENT_ID"), ldflags) 54 } 55 56 return run("go", "build", "-trimpath", "-ldflags", ldflags, "-o", exe, "./cmd/gh") 57 }, 58 "manpages": func(_ string) error { 59 return run("go", "run", "./cmd/gen-docs", "--man-page", "--doc-path", "./share/man/man1/") 60 }, 61 "clean": func(_ string) error { 62 return rmrf("bin", "share") 63 }, 64 } 65 66 var self string 67 68 func main() { 69 args := os.Args[:1] 70 for _, arg := range os.Args[1:] { 71 if idx := strings.IndexRune(arg, '='); idx >= 0 { 72 os.Setenv(arg[:idx], arg[idx+1:]) 73 } else { 74 args = append(args, arg) 75 } 76 } 77 78 if len(args) < 2 { 79 if isWindowsTarget() { 80 args = append(args, filepath.Join("bin", "gh.exe")) 81 } else { 82 args = append(args, "bin/gh") 83 } 84 } 85 86 self = filepath.Base(args[0]) 87 if self == "build" { 88 self = "build.go" 89 } 90 91 for _, task := range args[1:] { 92 t := tasks[normalizeTask(task)] 93 if t == nil { 94 fmt.Fprintf(os.Stderr, "Don't know how to build task `%s`.\n", task) 95 os.Exit(1) 96 } 97 98 err := t(task) 99 if err != nil { 100 fmt.Fprintln(os.Stderr, err) 101 fmt.Fprintf(os.Stderr, "%s: building task `%s` failed.\n", self, task) 102 os.Exit(1) 103 } 104 } 105 } 106 107 func isWindowsTarget() bool { 108 if os.Getenv("GOOS") == "windows" { 109 return true 110 } 111 if runtime.GOOS == "windows" { 112 return true 113 } 114 return false 115 } 116 117 func version() string { 118 if versionEnv := os.Getenv("GH_VERSION"); versionEnv != "" { 119 return versionEnv 120 } 121 if desc, err := cmdOutput("git", "describe", "--tags"); err == nil { 122 return desc 123 } 124 rev, _ := cmdOutput("git", "rev-parse", "--short", "HEAD") 125 return rev 126 } 127 128 func date() string { 129 t := time.Now() 130 if sourceDate := os.Getenv("SOURCE_DATE_EPOCH"); sourceDate != "" { 131 if sec, err := strconv.ParseInt(sourceDate, 10, 64); err == nil { 132 t = time.Unix(sec, 0) 133 } 134 } 135 return t.Format("2006-01-02") 136 } 137 138 func sourceFilesLaterThan(t time.Time) bool { 139 foundLater := false 140 err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 141 if err != nil { 142 // Ignore errors that occur when the project contains a symlink to a filesystem or volume that 143 // Windows doesn't have access to. 144 if path != "." && isAccessDenied(err) { 145 fmt.Fprintf(os.Stderr, "%s: %v\n", path, err) 146 return nil 147 } 148 return err 149 } 150 if foundLater { 151 return filepath.SkipDir 152 } 153 if len(path) > 1 && (path[0] == '.' || path[0] == '_') { 154 if info.IsDir() { 155 return filepath.SkipDir 156 } else { 157 return nil 158 } 159 } 160 if info.IsDir() { 161 if name := filepath.Base(path); name == "vendor" || name == "node_modules" { 162 return filepath.SkipDir 163 } 164 return nil 165 } 166 if path == "go.mod" || path == "go.sum" || (strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go")) { 167 if info.ModTime().After(t) { 168 foundLater = true 169 } 170 } 171 return nil 172 }) 173 if err != nil { 174 panic(err) 175 } 176 return foundLater 177 } 178 179 func isAccessDenied(err error) bool { 180 var pe *os.PathError 181 // we would use `syscall.ERROR_ACCESS_DENIED` if this script supported build tags 182 return errors.As(err, &pe) && strings.Contains(pe.Err.Error(), "Access is denied") 183 } 184 185 func rmrf(targets ...string) error { 186 args := append([]string{"rm", "-rf"}, targets...) 187 announce(args...) 188 for _, target := range targets { 189 if err := os.RemoveAll(target); err != nil { 190 return err 191 } 192 } 193 return nil 194 } 195 196 func announce(args ...string) { 197 fmt.Println(shellInspect(args)) 198 } 199 200 func run(args ...string) error { 201 exe, err := safeexec.LookPath(args[0]) 202 if err != nil { 203 return err 204 } 205 announce(args...) 206 cmd := exec.Command(exe, args[1:]...) 207 cmd.Stdout = os.Stdout 208 cmd.Stderr = os.Stderr 209 return cmd.Run() 210 } 211 212 func cmdOutput(args ...string) (string, error) { 213 exe, err := safeexec.LookPath(args[0]) 214 if err != nil { 215 return "", err 216 } 217 cmd := exec.Command(exe, args[1:]...) 218 cmd.Stderr = io.Discard 219 out, err := cmd.Output() 220 return strings.TrimSuffix(string(out), "\n"), err 221 } 222 223 func shellInspect(args []string) string { 224 fmtArgs := make([]string, len(args)) 225 for i, arg := range args { 226 if strings.ContainsAny(arg, " \t'\"") { 227 fmtArgs[i] = fmt.Sprintf("%q", arg) 228 } else { 229 fmtArgs[i] = arg 230 } 231 } 232 return strings.Join(fmtArgs, " ") 233 } 234 235 func normalizeTask(t string) string { 236 return filepath.ToSlash(strings.TrimSuffix(t, ".exe")) 237 }