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  }