...

Source file src/github.com/urfave/cli/v2/internal/build/build.go

Documentation: github.com/urfave/cli/v2/internal/build

     1  // local build script file, similar to a makefile or collection of bash scripts in other projects
     2  
     3  package main
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"log"
    12  	"math"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"runtime"
    19  	"strings"
    20  
    21  	"github.com/urfave/cli/v2"
    22  )
    23  
    24  const (
    25  	badNewsEmoji      = "🚨"
    26  	goodNewsEmoji     = "✨"
    27  	checksPassedEmoji = "✅"
    28  
    29  	gfmrunVersion = "v1.3.0"
    30  
    31  	v2diffWarning = `
    32  # The unified diff above indicates that the public API surface area
    33  # has changed. If you feel that the changes are acceptable and adhere
    34  # to the semantic versioning promise of the v2.x series described in
    35  # docs/CONTRIBUTING.md, please run the following command to promote
    36  # the current go docs:
    37  #
    38  #     make v2approve
    39  #
    40  `
    41  )
    42  
    43  func main() {
    44  	top, err := func() (string, error) {
    45  		if v, err := sh("git", "rev-parse", "--show-toplevel"); err == nil {
    46  			return strings.TrimSpace(v), nil
    47  		}
    48  
    49  		return os.Getwd()
    50  	}()
    51  	if err != nil {
    52  		log.Fatal(err)
    53  	}
    54  
    55  	app := &cli.App{
    56  		Name:  "builder",
    57  		Usage: "Do a thing for urfave/cli! (maybe build?)",
    58  		Commands: cli.Commands{
    59  			{
    60  				Name:   "vet",
    61  				Action: topRunAction("go", "vet", "./..."),
    62  			},
    63  			{
    64  				Name:   "test",
    65  				Action: TestActionFunc,
    66  			},
    67  			{
    68  				Name: "gfmrun",
    69  				Flags: []cli.Flag{
    70  					&cli.BoolFlag{
    71  						Name:  "walk",
    72  						Value: false,
    73  						Usage: "Walk the specified directory and perform validation on all markdown files",
    74  					},
    75  				},
    76  				Action: GfmrunActionFunc,
    77  			},
    78  			{
    79  				Name:   "check-binary-size",
    80  				Action: checkBinarySizeActionFunc,
    81  			},
    82  			{
    83  				Name:   "generate",
    84  				Action: GenerateActionFunc,
    85  				Usage:  "generate API docs",
    86  			},
    87  			{
    88  				Name: "yamlfmt",
    89  				Flags: []cli.Flag{
    90  					&cli.BoolFlag{Name: "strict", Value: false, Usage: "require presence of yq"},
    91  				},
    92  				Action: YAMLFmtActionFunc,
    93  			},
    94  			{
    95  				Name:   "diffcheck",
    96  				Action: DiffCheckActionFunc,
    97  			},
    98  			{
    99  				Name:   "ensure-goimports",
   100  				Action: EnsureGoimportsActionFunc,
   101  			},
   102  			{
   103  				Name:   "ensure-gfmrun",
   104  				Action: EnsureGfmrunActionFunc,
   105  			},
   106  			{
   107  				Name:   "ensure-mkdocs",
   108  				Action: EnsureMkdocsActionFunc,
   109  				Flags: []cli.Flag{
   110  					&cli.BoolFlag{Name: "upgrade-pip"},
   111  				},
   112  			},
   113  			{
   114  				Name:   "set-mkdocs-remote",
   115  				Action: SetMkdocsRemoteActionFunc,
   116  				Flags: []cli.Flag{
   117  					&cli.StringFlag{
   118  						Name:     "github-token",
   119  						EnvVars:  []string{"MKDOCS_REMOTE_GITHUB_TOKEN"},
   120  						Required: true,
   121  					},
   122  				},
   123  			},
   124  			{
   125  				Name:   "deploy-mkdocs",
   126  				Action: topRunAction("mkdocs", "gh-deploy", "--force"),
   127  			},
   128  			{
   129  				Name:   "lint",
   130  				Action: LintActionFunc,
   131  			},
   132  			{
   133  				Name: "v2diff",
   134  				Flags: []cli.Flag{
   135  					&cli.BoolFlag{Name: "color", Value: false},
   136  				},
   137  				Action: V2Diff,
   138  			},
   139  			{
   140  				Name: "v2approve",
   141  				Action: topRunAction(
   142  					"cp",
   143  					"-v",
   144  					"godoc-current.txt",
   145  					filepath.Join("testdata", "godoc-v2.x.txt"),
   146  				),
   147  			},
   148  		},
   149  		Flags: []cli.Flag{
   150  			&cli.StringFlag{
   151  				Name:  "tags",
   152  				Usage: "set build tags",
   153  			},
   154  			&cli.PathFlag{
   155  				Name:  "top",
   156  				Value: top,
   157  			},
   158  			&cli.StringSliceFlag{
   159  				Name:  "packages",
   160  				Value: cli.NewStringSlice("cli", "altsrc", "internal/build"),
   161  			},
   162  		},
   163  	}
   164  
   165  	if err := app.Run(os.Args); err != nil {
   166  		log.Fatal(err)
   167  	}
   168  }
   169  
   170  func sh(exe string, args ...string) (string, error) {
   171  	cmd := exec.Command(exe, args...)
   172  	cmd.Stdin = os.Stdin
   173  	cmd.Stderr = os.Stderr
   174  
   175  	fmt.Fprintf(os.Stderr, "# ---> %s\n", cmd)
   176  	outBytes, err := cmd.Output()
   177  	return string(outBytes), err
   178  }
   179  
   180  func topRunAction(arg string, args ...string) cli.ActionFunc {
   181  	return func(cCtx *cli.Context) error {
   182  		if err := os.Chdir(cCtx.Path("top")); err != nil {
   183  			return err
   184  		}
   185  
   186  		return runCmd(arg, args...)
   187  	}
   188  }
   189  
   190  func runCmd(arg string, args ...string) error {
   191  	cmd := exec.Command(arg, args...)
   192  
   193  	cmd.Stdin = os.Stdin
   194  	cmd.Stdout = os.Stdout
   195  	cmd.Stderr = os.Stderr
   196  
   197  	fmt.Fprintf(os.Stderr, "# ---> %s\n", cmd)
   198  	return cmd.Run()
   199  }
   200  
   201  func downloadFile(src, dest string, dirPerm, perm os.FileMode) error {
   202  	req, err := http.NewRequest(http.MethodGet, src, nil)
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	resp, err := http.DefaultClient.Do(req)
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	defer resp.Body.Close()
   213  
   214  	if resp.StatusCode >= 300 {
   215  		return fmt.Errorf("download response %[1]v", resp.StatusCode)
   216  	}
   217  
   218  	if err := os.MkdirAll(filepath.Dir(dest), dirPerm); err != nil {
   219  		return err
   220  	}
   221  
   222  	out, err := os.Create(dest)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	if _, err := io.Copy(out, resp.Body); err != nil {
   228  		return err
   229  	}
   230  
   231  	if err := out.Close(); err != nil {
   232  		return err
   233  	}
   234  
   235  	return os.Chmod(dest, perm)
   236  }
   237  
   238  func VetActionFunc(cCtx *cli.Context) error {
   239  	return runCmd("go", "vet", cCtx.Path("top")+"/...")
   240  }
   241  
   242  func TestActionFunc(c *cli.Context) error {
   243  	tags := c.String("tags")
   244  
   245  	for _, pkg := range c.StringSlice("packages") {
   246  		packageName := "github.com/urfave/cli/v2"
   247  
   248  		if pkg != "cli" {
   249  			packageName = fmt.Sprintf("github.com/urfave/cli/v2/%s", pkg)
   250  		}
   251  
   252  		args := []string{"test"}
   253  		if tags != "" {
   254  			args = append(args, []string{"-tags", tags}...)
   255  		}
   256  
   257  		args = append(args, []string{
   258  			"-v",
   259  			"--coverprofile", pkg + ".coverprofile",
   260  			"--covermode", "count",
   261  			"--cover", packageName,
   262  			packageName,
   263  		}...)
   264  
   265  		if err := runCmd("go", args...); err != nil {
   266  			return err
   267  		}
   268  	}
   269  
   270  	return testCleanup(c.StringSlice("packages"))
   271  }
   272  
   273  func testCleanup(packages []string) error {
   274  	out := &bytes.Buffer{}
   275  
   276  	fmt.Fprintf(out, "mode: count\n")
   277  
   278  	for _, pkg := range packages {
   279  		filename := pkg + ".coverprofile"
   280  
   281  		lineBytes, err := os.ReadFile(filename)
   282  		if err != nil {
   283  			return err
   284  		}
   285  
   286  		lines := strings.Split(string(lineBytes), "\n")
   287  
   288  		fmt.Fprint(out, strings.Join(lines[1:], "\n"))
   289  
   290  		if err := os.Remove(filename); err != nil {
   291  			return err
   292  		}
   293  	}
   294  
   295  	return os.WriteFile("coverage.txt", out.Bytes(), 0644)
   296  }
   297  
   298  func GfmrunActionFunc(cCtx *cli.Context) error {
   299  	top := cCtx.Path("top")
   300  
   301  	bash, err := exec.LookPath("bash")
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	os.Setenv("SHELL", bash)
   307  
   308  	tmpDir, err := os.MkdirTemp("", "urfave-cli*")
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	wd, err := os.Getwd()
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	if err := os.Chdir(tmpDir); err != nil {
   319  		return err
   320  	}
   321  
   322  	fmt.Fprintf(cCtx.App.ErrWriter, "# ---> workspace/TMPDIR is %q\n", tmpDir)
   323  
   324  	if err := runCmd("go", "work", "init", top); err != nil {
   325  		return err
   326  	}
   327  
   328  	os.Setenv("TMPDIR", tmpDir)
   329  
   330  	if err := os.Chdir(wd); err != nil {
   331  		return err
   332  	}
   333  
   334  	dirPath := cCtx.Args().Get(0)
   335  	if dirPath == "" {
   336  		dirPath = "README.md"
   337  	}
   338  
   339  	walk := cCtx.Bool("walk")
   340  	sources := []string{}
   341  
   342  	if walk {
   343  		// Walk the directory and find all markdown files.
   344  		err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
   345  			if err != nil {
   346  				return err
   347  			}
   348  
   349  			if info.IsDir() {
   350  				return nil
   351  			}
   352  
   353  			if filepath.Ext(path) != ".md" {
   354  				return nil
   355  			}
   356  
   357  			sources = append(sources, path)
   358  			return nil
   359  		})
   360  		if err != nil {
   361  			return err
   362  		}
   363  	} else {
   364  		sources = append(sources, dirPath)
   365  	}
   366  
   367  	var counter int
   368  
   369  	for _, src := range sources {
   370  		file, err := os.Open(src)
   371  		if err != nil {
   372  			return err
   373  		}
   374  		defer file.Close()
   375  
   376  		scanner := bufio.NewScanner(file)
   377  		for scanner.Scan() {
   378  			if strings.Contains(scanner.Text(), "package main") {
   379  				counter++
   380  			}
   381  		}
   382  
   383  		err = file.Close()
   384  		if err != nil {
   385  			return err
   386  		}
   387  
   388  		err = scanner.Err()
   389  		if err != nil {
   390  			return err
   391  		}
   392  	}
   393  
   394  	gfmArgs := []string{
   395  		"--count",
   396  		fmt.Sprint(counter),
   397  	}
   398  	for _, src := range sources {
   399  		gfmArgs = append(gfmArgs, "--sources", src)
   400  	}
   401  
   402  	if err := runCmd("gfmrun", gfmArgs...); err != nil {
   403  		return err
   404  	}
   405  
   406  	return os.RemoveAll(tmpDir)
   407  }
   408  
   409  // checkBinarySizeActionFunc checks the size of an example binary to ensure that we are keeping size down
   410  // this was originally inspired by https://github.com/urfave/cli/issues/1055, and followed up on as a part
   411  // of https://github.com/urfave/cli/issues/1057
   412  func checkBinarySizeActionFunc(c *cli.Context) (err error) {
   413  	const (
   414  		cliSourceFilePath    = "./internal/example-cli/example-cli.go"
   415  		cliBuiltFilePath     = "./internal/example-cli/built-example"
   416  		helloSourceFilePath  = "./internal/example-hello-world/example-hello-world.go"
   417  		helloBuiltFilePath   = "./internal/example-hello-world/built-example"
   418  		desiredMaxBinarySize = 2.2
   419  		mbStringFormatter    = "%.1fMB"
   420  	)
   421  
   422  	desiredMinBinarySize := 1.675
   423  
   424  	tags := c.String("tags")
   425  
   426  	if strings.Contains(tags, "urfave_cli_no_docs") {
   427  		desiredMinBinarySize = 1.39
   428  	}
   429  
   430  	// get cli example size
   431  	cliSize, err := getSize(cliSourceFilePath, cliBuiltFilePath, tags)
   432  	if err != nil {
   433  		return err
   434  	}
   435  
   436  	// get hello world size
   437  	helloSize, err := getSize(helloSourceFilePath, helloBuiltFilePath, tags)
   438  	if err != nil {
   439  		return err
   440  	}
   441  
   442  	// The CLI size diff is the number we are interested in.
   443  	// This tells us how much our CLI package contributes to the binary size.
   444  	cliSizeDiff := cliSize - helloSize
   445  
   446  	// get human readable size, in MB with one decimal place.
   447  	// example output is: 35.2MB. (note: this simply an example)
   448  	// that output is much easier to reason about than the `35223432`
   449  	// that you would see output without the rounding
   450  	fileSizeInMB := float64(cliSizeDiff) / float64(1000000)
   451  	roundedFileSize := math.Round(fileSizeInMB*10) / 10
   452  	roundedFileSizeString := fmt.Sprintf(mbStringFormatter, roundedFileSize)
   453  
   454  	// check against bounds
   455  	isLessThanDesiredMin := roundedFileSize < desiredMinBinarySize
   456  	isMoreThanDesiredMax := roundedFileSize > desiredMaxBinarySize
   457  	desiredMinSizeString := fmt.Sprintf(mbStringFormatter, desiredMinBinarySize)
   458  	desiredMaxSizeString := fmt.Sprintf(mbStringFormatter, desiredMaxBinarySize)
   459  
   460  	// show guidance
   461  	fmt.Printf("\n%s is the current binary size\n", roundedFileSizeString)
   462  	// show guidance for min size
   463  	if isLessThanDesiredMin {
   464  		fmt.Printf("  %s %s is the target min size\n", goodNewsEmoji, desiredMinSizeString)
   465  		fmt.Println("") // visual spacing
   466  		fmt.Println("     The binary is smaller than the target min size, which is great news!")
   467  		fmt.Println("     That means that your changes are shrinking the binary size.")
   468  		fmt.Println("     You'll want to go into ./internal/build/build.go and decrease")
   469  		fmt.Println("     the desiredMinBinarySize, and also probably decrease the ")
   470  		fmt.Println("     desiredMaxBinarySize by the same amount. That will ensure that")
   471  		fmt.Println("     future PRs will enforce the newly shrunk binary sizes.")
   472  		fmt.Println("") // visual spacing
   473  		os.Exit(1)
   474  	} else {
   475  		fmt.Printf("  %s %s is the target min size\n", checksPassedEmoji, desiredMinSizeString)
   476  	}
   477  	// show guidance for max size
   478  	if isMoreThanDesiredMax {
   479  		fmt.Printf("  %s %s is the target max size\n", badNewsEmoji, desiredMaxSizeString)
   480  		fmt.Println("") // visual spacing
   481  		fmt.Println("     The binary is larger than the target max size.")
   482  		fmt.Println("     That means that your changes are increasing the binary size.")
   483  		fmt.Println("     The first thing you'll want to do is ask your yourself")
   484  		fmt.Println("     Is this change worth increasing the binary size?")
   485  		fmt.Println("     Larger binary sizes for this package can dissuade its use.")
   486  		fmt.Println("     If this change is worth the increase, then we can up the")
   487  		fmt.Println("     desired max binary size. To do that you'll want to go into")
   488  		fmt.Println("     ./internal/build/build.go and increase the desiredMaxBinarySize,")
   489  		fmt.Println("     and increase the desiredMinBinarySize by the same amount.")
   490  		fmt.Println("") // visual spacing
   491  		os.Exit(1)
   492  	} else {
   493  		fmt.Printf("  %s %s is the target max size\n", checksPassedEmoji, desiredMaxSizeString)
   494  	}
   495  
   496  	return nil
   497  }
   498  
   499  func GenerateActionFunc(cCtx *cli.Context) error {
   500  	top := cCtx.Path("top")
   501  
   502  	log.Println("--- generating godoc-current.txt API reference ---")
   503  	cliDocs, err := sh("go", "doc", "-all", top)
   504  	if err != nil {
   505  		return err
   506  	}
   507  
   508  	altsrcDocs, err := sh("go", "doc", "-all", filepath.Join(top, "altsrc"))
   509  	if err != nil {
   510  		return err
   511  	}
   512  
   513  	if err := os.WriteFile(
   514  		filepath.Join(top, "godoc-current.txt"),
   515  		[]byte(cliDocs+altsrcDocs),
   516  		0644,
   517  	); err != nil {
   518  		return err
   519  	}
   520  
   521  	log.Println("--- generating Go source files ---")
   522  	return runCmd("go", "generate", cCtx.Path("top")+"/...")
   523  }
   524  
   525  func YAMLFmtActionFunc(cCtx *cli.Context) error {
   526  	yqBin, err := exec.LookPath("yq")
   527  	if err != nil {
   528  		if !cCtx.Bool("strict") {
   529  			fmt.Fprintln(cCtx.App.ErrWriter, "# ---> no yq found; skipping")
   530  			return nil
   531  		}
   532  
   533  		return err
   534  	}
   535  
   536  	if err := os.Chdir(cCtx.Path("top")); err != nil {
   537  		return err
   538  	}
   539  
   540  	return runCmd(yqBin, "eval", "--inplace", "flag-spec.yaml")
   541  }
   542  
   543  func DiffCheckActionFunc(cCtx *cli.Context) error {
   544  	if err := os.Chdir(cCtx.Path("top")); err != nil {
   545  		return err
   546  	}
   547  
   548  	if err := runCmd("git", "diff", "--exit-code"); err != nil {
   549  		return err
   550  	}
   551  
   552  	return runCmd("git", "diff", "--cached", "--exit-code")
   553  }
   554  
   555  func EnsureGoimportsActionFunc(cCtx *cli.Context) error {
   556  	top := cCtx.Path("top")
   557  	if err := os.Chdir(top); err != nil {
   558  		return err
   559  	}
   560  
   561  	if err := runCmd(
   562  		"goimports",
   563  		"-d",
   564  		filepath.Join(top, "internal/build/build.go"),
   565  	); err == nil {
   566  		return nil
   567  	}
   568  
   569  	os.Setenv("GOBIN", filepath.Join(top, ".local/bin"))
   570  
   571  	return runCmd("go", "install", "golang.org/x/tools/cmd/goimports@latest")
   572  }
   573  
   574  func EnsureGfmrunActionFunc(cCtx *cli.Context) error {
   575  	top := cCtx.Path("top")
   576  	gfmrunExe := filepath.Join(top, ".local/bin/gfmrun")
   577  
   578  	if err := os.Chdir(top); err != nil {
   579  		return err
   580  	}
   581  
   582  	if v, err := sh(gfmrunExe, "--version"); err == nil && strings.TrimSpace(v) == gfmrunVersion {
   583  		return nil
   584  	}
   585  
   586  	gfmrunURL, err := url.Parse(
   587  		fmt.Sprintf(
   588  			"https://github.com/urfave/gfmrun/releases/download/%[1]s/gfmrun-%[2]s-%[3]s-%[1]s",
   589  			gfmrunVersion, runtime.GOOS, runtime.GOARCH,
   590  		),
   591  	)
   592  	if err != nil {
   593  		return err
   594  	}
   595  
   596  	return downloadFile(gfmrunURL.String(), gfmrunExe, 0755, 0755)
   597  }
   598  
   599  func EnsureMkdocsActionFunc(cCtx *cli.Context) error {
   600  	if err := os.Chdir(cCtx.Path("top")); err != nil {
   601  		return err
   602  	}
   603  
   604  	if err := runCmd("mkdocs", "--version"); err == nil {
   605  		return nil
   606  	}
   607  
   608  	if cCtx.Bool("upgrade-pip") {
   609  		if err := runCmd("pip", "install", "-U", "pip"); err != nil {
   610  			return err
   611  		}
   612  	}
   613  
   614  	return runCmd("pip", "install", "-r", "mkdocs-reqs.txt")
   615  }
   616  
   617  func SetMkdocsRemoteActionFunc(cCtx *cli.Context) error {
   618  	ghToken := strings.TrimSpace(cCtx.String("github-token"))
   619  	if ghToken == "" {
   620  		return errors.New("empty github token")
   621  	}
   622  
   623  	if err := os.Chdir(cCtx.Path("top")); err != nil {
   624  		return err
   625  	}
   626  
   627  	if err := runCmd("git", "remote", "rm", "origin"); err != nil {
   628  		return err
   629  	}
   630  
   631  	return runCmd(
   632  		"git", "remote", "add", "origin",
   633  		fmt.Sprintf("https://x-access-token:%[1]s@github.com/urfave/cli.git", ghToken),
   634  	)
   635  }
   636  
   637  func LintActionFunc(cCtx *cli.Context) error {
   638  	top := cCtx.Path("top")
   639  	if err := os.Chdir(top); err != nil {
   640  		return err
   641  	}
   642  
   643  	out, err := sh(filepath.Join(top, ".local/bin/goimports"), "-l", ".")
   644  	if err != nil {
   645  		return err
   646  	}
   647  
   648  	if strings.TrimSpace(out) != "" {
   649  		fmt.Fprintln(cCtx.App.ErrWriter, "# ---> goimports -l is non-empty:")
   650  		fmt.Fprintln(cCtx.App.ErrWriter, out)
   651  
   652  		return errors.New("goimports needed")
   653  	}
   654  
   655  	return nil
   656  }
   657  
   658  func V2Diff(cCtx *cli.Context) error {
   659  	if err := os.Chdir(cCtx.Path("top")); err != nil {
   660  		return err
   661  	}
   662  
   663  	err := runCmd(
   664  		"diff",
   665  		"--ignore-all-space",
   666  		"--minimal",
   667  		"--color="+func() string {
   668  			if cCtx.Bool("color") {
   669  				return "always"
   670  			}
   671  			return "auto"
   672  		}(),
   673  		"--unified",
   674  		"--label=a/godoc",
   675  		filepath.Join("testdata", "godoc-v2.x.txt"),
   676  		"--label=b/godoc",
   677  		"godoc-current.txt",
   678  	)
   679  
   680  	if err != nil {
   681  		fmt.Printf("# %v ---> Hey! <---\n", badNewsEmoji)
   682  		fmt.Println(strings.TrimSpace(v2diffWarning))
   683  	}
   684  
   685  	return err
   686  }
   687  
   688  func getSize(sourcePath, builtPath, tags string) (int64, error) {
   689  	args := []string{"build"}
   690  
   691  	if tags != "" {
   692  		args = append(args, []string{"-tags", tags}...)
   693  	}
   694  
   695  	args = append(args, []string{
   696  		"-o", builtPath,
   697  		"-ldflags", "-s -w",
   698  		sourcePath,
   699  	}...)
   700  
   701  	if err := runCmd("go", args...); err != nil {
   702  		fmt.Println("issue getting size for example binary")
   703  		return 0, err
   704  	}
   705  
   706  	fileInfo, err := os.Stat(builtPath)
   707  	if err != nil {
   708  		fmt.Println("issue getting size for example binary")
   709  		return 0, err
   710  	}
   711  
   712  	return fileInfo.Size(), nil
   713  }
   714  

View as plain text