github.com/goplus/gop@v1.2.6/cmd/make.go (about)

     1  //go:build ignore
     2  // +build ignore
     3  
     4  /*
     5   * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
     6   *
     7   * Licensed under the Apache License, Version 2.0 (the "License");
     8   * you may not use this file except in compliance with the License.
     9   * You may obtain a copy of the License at
    10   *
    11   *     http://www.apache.org/licenses/LICENSE-2.0
    12   *
    13   * Unless required by applicable law or agreed to in writing, software
    14   * distributed under the License is distributed on an "AS IS" BASIS,
    15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16   * See the License for the specific language governing permissions and
    17   * limitations under the License.
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"bytes"
    24  	"flag"
    25  	"fmt"
    26  	"log"
    27  	"os"
    28  	"os/exec"
    29  	"path/filepath"
    30  	"regexp"
    31  	"runtime"
    32  	"strings"
    33  	"time"
    34  )
    35  
    36  func checkPathExist(path string, isDir bool) bool {
    37  	stat, err := os.Lstat(path) // Note: os.Lstat() will not follow the symbolic link.
    38  	isExists := !os.IsNotExist(err)
    39  	if isDir {
    40  		return isExists && stat.IsDir()
    41  	}
    42  	return isExists && !stat.IsDir()
    43  }
    44  
    45  func trimRight(s string) string {
    46  	return strings.TrimRight(s, " \t\r\n")
    47  }
    48  
    49  // Path returns single path to check
    50  type Path struct {
    51  	path  string
    52  	isDir bool
    53  }
    54  
    55  func (p *Path) checkExists(rootDir string) bool {
    56  	absPath := filepath.Join(rootDir, p.path)
    57  	return checkPathExist(absPath, p.isDir)
    58  }
    59  
    60  func getGopRoot() string {
    61  	pwd, _ := os.Getwd()
    62  
    63  	pathsToCheck := []Path{
    64  		{path: "cmd/gop", isDir: true},
    65  		{path: "builtin", isDir: true},
    66  		{path: "go.mod", isDir: false},
    67  		{path: "go.sum", isDir: false},
    68  	}
    69  
    70  	for _, path := range pathsToCheck {
    71  		if !path.checkExists(pwd) {
    72  			println("Error: This script should be run at the root directory of gop repository.")
    73  			os.Exit(1)
    74  		}
    75  	}
    76  	return pwd
    77  }
    78  
    79  var gopRoot = getGopRoot()
    80  var initCommandExecuteEnv = os.Environ()
    81  var commandExecuteEnv = initCommandExecuteEnv
    82  
    83  // Always put `gop` command as the first item, as it will be referenced by below code.
    84  var gopBinFiles = []string{"gop", "gopfmt"}
    85  
    86  const (
    87  	inWindows = (runtime.GOOS == "windows")
    88  )
    89  
    90  func init() {
    91  	if inWindows {
    92  		for index, file := range gopBinFiles {
    93  			file += ".exe"
    94  			gopBinFiles[index] = file
    95  		}
    96  	}
    97  }
    98  
    99  type ExecCmdError struct {
   100  	Err    error
   101  	Stderr string
   102  }
   103  
   104  func (p *ExecCmdError) Error() string {
   105  	if e := p.Stderr; e != "" {
   106  		return e
   107  	}
   108  	return p.Err.Error()
   109  }
   110  
   111  type iGitRemote interface {
   112  	CheckRemoteUrl()
   113  	CreateBranchFrom(branch, remote string) error
   114  	PushCommits(remote, branch string) error
   115  	DeleteBranch(branch string) error
   116  }
   117  
   118  type (
   119  	gitRemoteImpl struct{}
   120  	gitRemoteNone struct{}
   121  )
   122  
   123  func (p *gitRemoteImpl) CheckRemoteUrl() {
   124  	if getGitRemoteUrl("gop") == "" {
   125  		log.Fatalln("Error: git remote gop not found, please use `git remote add gop git@github.com:goplus/gop.git`.")
   126  	}
   127  }
   128  
   129  func (p *gitRemoteImpl) CreateBranchFrom(branch, remote string) (err error) {
   130  	_, err = execCommand("git", "fetch", remote)
   131  	if err != nil {
   132  		return
   133  	}
   134  	execCommand("git", "branch", "-D", branch)
   135  	_, err = execCommand("git", "checkout", "-b", branch, remote+"/"+branch)
   136  	return
   137  }
   138  
   139  func (p *gitRemoteImpl) PushCommits(remote, branch string) error {
   140  	_, err := execCommand("git", "push", remote, branch)
   141  	return err
   142  }
   143  
   144  func (p *gitRemoteImpl) DeleteBranch(branch string) error {
   145  	_, err := execCommand("git", "branch", "-D", branch)
   146  	return err
   147  }
   148  
   149  func (p *gitRemoteNone) CheckRemoteUrl()                                    {}
   150  func (p *gitRemoteNone) CreateBranchFrom(branch, remote string) (err error) { return nil }
   151  func (p *gitRemoteNone) PushCommits(remote, branch string) error            { return nil }
   152  func (p *gitRemoteNone) DeleteBranch(branch string) error                   { return nil }
   153  
   154  var (
   155  	gitRemote iGitRemote = &gitRemoteImpl{}
   156  )
   157  
   158  func execCommand(command string, arg ...string) (string, error) {
   159  	var stdout, stderr bytes.Buffer
   160  	cmd := exec.Command(command, arg...)
   161  	cmd.Stdout = &stdout
   162  	cmd.Stderr = &stderr
   163  	cmd.Env = commandExecuteEnv
   164  	err := cmd.Run()
   165  	if err != nil {
   166  		err = &ExecCmdError{Err: err, Stderr: stderr.String()}
   167  	}
   168  	return stdout.String(), err
   169  }
   170  
   171  func getTagRev(tag string) string {
   172  	const commit = "commit "
   173  	stdout, err := execCommand("git", "show", tag)
   174  	if err != nil || !strings.HasPrefix(stdout, commit) {
   175  		return ""
   176  	}
   177  	data := stdout[len(commit):]
   178  	if pos := strings.IndexByte(data, '\n'); pos > 0 {
   179  		return data[:pos]
   180  	}
   181  	return ""
   182  }
   183  
   184  func getGitRemoteUrl(name string) string {
   185  	stdout, err := execCommand("git", "remote", "get-url", name)
   186  	if err != nil {
   187  		return ""
   188  	}
   189  	return stdout
   190  }
   191  
   192  func getGitBranch() string {
   193  	stdout, err := execCommand("git", "rev-parse", "--abbrev-ref", "HEAD")
   194  	if err != nil {
   195  		return ""
   196  	}
   197  	return trimRight(stdout)
   198  }
   199  
   200  func gitTag(tag string) error {
   201  	_, err := execCommand("git", "tag", tag)
   202  	return err
   203  }
   204  
   205  func gitTagAndPushTo(tag string, remote, branch string) error {
   206  	if err := gitRemote.PushCommits(remote, branch); err != nil {
   207  		return err
   208  	}
   209  	if err := gitTag(tag); err != nil {
   210  		return err
   211  	}
   212  	return gitRemote.PushCommits(remote, tag)
   213  }
   214  
   215  func gitAdd(file string) error {
   216  	_, err := execCommand("git", "add", file)
   217  	return err
   218  }
   219  
   220  func gitCommit(msg string) error {
   221  	out, err := execCommand("git", "commit", "-a", "-m", msg)
   222  	if err != nil {
   223  		if e := err.(*ExecCmdError); e.Stderr == "" {
   224  			e.Stderr = out
   225  		}
   226  	}
   227  	return err
   228  }
   229  
   230  func gitCheckoutBranch(branch string) error {
   231  	_, err := execCommand("git", "checkout", branch)
   232  	return err
   233  }
   234  
   235  func isGitRepo() bool {
   236  	gitDir, err := execCommand("git", "rev-parse", "--git-dir")
   237  	if err != nil {
   238  		return false
   239  	}
   240  	return checkPathExist(filepath.Join(gopRoot, trimRight(gitDir)), true)
   241  }
   242  
   243  func getBuildDateTime() string {
   244  	now := time.Now()
   245  	return now.Format("2006-01-02_15-04-05")
   246  }
   247  
   248  func getBuildVer() string {
   249  	latestTagCommit, err := execCommand("git", "rev-list", "--tags", "--max-count=1")
   250  	if err != nil {
   251  		return ""
   252  	}
   253  
   254  	stdout, err := execCommand("git", "describe", "--tags", trimRight(latestTagCommit))
   255  	if err != nil {
   256  		return ""
   257  	}
   258  
   259  	return fmt.Sprintf("%s devel", trimRight(stdout))
   260  }
   261  
   262  func getGopBuildFlags() string {
   263  	defaultGopRoot := gopRoot
   264  	if gopRootFinal := os.Getenv("GOPROOT_FINAL"); gopRootFinal != "" {
   265  		defaultGopRoot = gopRootFinal
   266  	}
   267  	buildFlags := fmt.Sprintf("-X \"github.com/goplus/gop/env.defaultGopRoot=%s\"", defaultGopRoot)
   268  	buildFlags += fmt.Sprintf(" -X \"github.com/goplus/gop/env.buildDate=%s\"", getBuildDateTime())
   269  
   270  	version := findGopVersion()
   271  	buildFlags += fmt.Sprintf(" -X \"github.com/goplus/gop/env.buildVersion=%s\"", version)
   272  
   273  	return buildFlags
   274  }
   275  
   276  func detectGopBinPath() string {
   277  	return filepath.Join(gopRoot, "bin")
   278  }
   279  
   280  func detectGoBinPath() string {
   281  	goBin, ok := os.LookupEnv("GOBIN")
   282  	if ok {
   283  		return goBin
   284  	}
   285  
   286  	goPath, ok := os.LookupEnv("GOPATH")
   287  	if ok {
   288  		list := filepath.SplitList(goPath)
   289  		if len(list) > 0 {
   290  			// Put in first directory of $GOPATH.
   291  			return filepath.Join(list[0], "bin")
   292  		}
   293  	}
   294  
   295  	homeDir, _ := os.UserHomeDir()
   296  	return filepath.Join(homeDir, "go", "bin")
   297  }
   298  
   299  func linkGoplusToLocalBin() string {
   300  	println("Start Linking.")
   301  
   302  	gopBinPath := detectGopBinPath()
   303  	goBinPath := detectGoBinPath()
   304  	if !checkPathExist(gopBinPath, true) {
   305  		log.Fatalf("Error: %s is not existed, you should build Go+ before linking.\n", gopBinPath)
   306  	}
   307  	if !checkPathExist(goBinPath, true) {
   308  		if err := os.MkdirAll(goBinPath, 0755); err != nil {
   309  			fmt.Printf("Error: target directory %s is not existed and we can't create one.\n", goBinPath)
   310  			log.Fatalln(err)
   311  		}
   312  	}
   313  
   314  	for _, file := range gopBinFiles {
   315  		sourceFile := filepath.Join(gopBinPath, file)
   316  		if !checkPathExist(sourceFile, false) {
   317  			log.Fatalf("Error: %s is not existed, you should build Go+ before linking.\n", sourceFile)
   318  		}
   319  		targetLink := filepath.Join(goBinPath, file)
   320  		if checkPathExist(targetLink, false) {
   321  			// Delete existed one
   322  			if err := os.Remove(targetLink); err != nil {
   323  				log.Fatalln(err)
   324  			}
   325  		}
   326  		if err := os.Symlink(sourceFile, targetLink); err != nil {
   327  			log.Fatalln(err)
   328  		}
   329  		fmt.Printf("Link %s to %s successfully.\n", sourceFile, targetLink)
   330  	}
   331  
   332  	println("End linking.")
   333  	return goBinPath
   334  }
   335  
   336  func buildGoplusTools(useGoProxy bool) {
   337  	commandsDir := filepath.Join(gopRoot, "cmd")
   338  	buildFlags := getGopBuildFlags()
   339  
   340  	if useGoProxy {
   341  		println("Info: we will use goproxy.cn as a Go proxy to accelerate installing process.")
   342  		commandExecuteEnv = append(commandExecuteEnv,
   343  			"GOPROXY=https://goproxy.cn,direct",
   344  		)
   345  	}
   346  
   347  	// Install Go+ binary files under current ./bin directory.
   348  	gopBinPath := detectGopBinPath()
   349  	if err := os.Mkdir(gopBinPath, 0755); err != nil && !os.IsExist(err) {
   350  		println("Error: Go+ can't create ./bin directory to put build assets.")
   351  		log.Fatalln(err)
   352  	}
   353  
   354  	println("Building Go+ tools...\n")
   355  	os.Chdir(commandsDir)
   356  	buildOutput, err := execCommand("go", "build", "-o", gopBinPath, "-v", "-ldflags", buildFlags, "./...")
   357  	if err != nil {
   358  		log.Fatalln(err)
   359  	}
   360  	print(buildOutput)
   361  
   362  	// Clear gop run cache
   363  	cleanGopRunCache()
   364  
   365  	println("\nGo+ tools built successfully!")
   366  }
   367  
   368  func showHelpPostInstall(installPath string) {
   369  	println("\nNEXT STEP:")
   370  	println("\nWe just installed Go+ into the directory: ", installPath)
   371  	message := `
   372  To setup a better Go+ development environment,
   373  we recommend you add the above install directory into your PATH environment variable.
   374  	`
   375  	println(message)
   376  }
   377  
   378  // Install Go+ tools
   379  func install() {
   380  	installPath := linkGoplusToLocalBin()
   381  
   382  	println("\nGo+ tools installed successfully!")
   383  
   384  	if _, err := execCommand("gop", "version"); err != nil {
   385  		showHelpPostInstall(installPath)
   386  	}
   387  }
   388  
   389  func runTestcases() {
   390  	println("Start running testcases.")
   391  	os.Chdir(gopRoot)
   392  
   393  	coverage := "-coverprofile=coverage.txt"
   394  	gopCommand := filepath.Join(detectGopBinPath(), gopBinFiles[0])
   395  	if !checkPathExist(gopCommand, false) {
   396  		println("Error: Go+ must be installed before running testcases.")
   397  		os.Exit(1)
   398  	}
   399  
   400  	testOutput, err := execCommand(gopCommand, "test", coverage, "-covermode=atomic", "./...")
   401  	println(testOutput)
   402  	if err != nil {
   403  		println(err.Error())
   404  	}
   405  
   406  	println("End running testcases.")
   407  }
   408  
   409  func clean() {
   410  	gopBinPath := detectGopBinPath()
   411  	goBinPath := detectGoBinPath()
   412  
   413  	// Clean links
   414  	for _, file := range gopBinFiles {
   415  		targetLink := filepath.Join(goBinPath, file)
   416  		if checkPathExist(targetLink, false) {
   417  			if err := os.Remove(targetLink); err != nil {
   418  				log.Fatalln(err)
   419  			}
   420  		}
   421  	}
   422  
   423  	// Clean build binary files
   424  	if checkPathExist(gopBinPath, true) {
   425  		if err := os.RemoveAll(gopBinPath); err != nil {
   426  			log.Fatalln(err)
   427  		}
   428  	}
   429  
   430  	cleanGopRunCache()
   431  }
   432  
   433  func cleanGopRunCache() {
   434  	homeDir, _ := os.UserHomeDir()
   435  	runCacheDir := filepath.Join(homeDir, ".gop", "run")
   436  	files := []string{"go.mod", "go.sum"}
   437  	for _, file := range files {
   438  		fullPath := filepath.Join(runCacheDir, file)
   439  		if checkPathExist(fullPath, false) {
   440  			if err := os.Remove(fullPath); err != nil {
   441  				log.Fatalln(err)
   442  			}
   443  		}
   444  	}
   445  }
   446  
   447  func uninstall() {
   448  	println("Uninstalling Go+ and related tools.")
   449  	clean()
   450  	println("Go+ and related tools uninstalled successfully.")
   451  }
   452  
   453  func isInChinaWindows() bool {
   454  	// Run `systeminfo` command on windows to check locale.
   455  	out, err := execCommand("systeminfo")
   456  	if err != nil {
   457  		fmt.Println("Run [systeminfo] command failed with error: ", err)
   458  		return false
   459  	}
   460  	// Check if output contains `zh-cn;`
   461  	return strings.Contains(out, "zh-cn;")
   462  }
   463  
   464  func isInChina() bool {
   465  	if inWindows {
   466  		return isInChinaWindows()
   467  	}
   468  	const prefix = "LANG=\""
   469  	out, err := execCommand("locale")
   470  	if err != nil {
   471  		return false
   472  	}
   473  	if strings.HasPrefix(out, prefix) {
   474  		out = out[len(prefix):]
   475  		return strings.HasPrefix(out, "zh_CN")
   476  	}
   477  	return false
   478  }
   479  
   480  // findGopVersion returns current version of gop
   481  func findGopVersion() string {
   482  	versionFile := filepath.Join(gopRoot, "VERSION")
   483  	// Read version from VERSION file
   484  	data, err := os.ReadFile(versionFile)
   485  	if err == nil {
   486  		version := trimRight(string(data))
   487  		return version
   488  	}
   489  
   490  	// Read version from git repo
   491  	if !isGitRepo() {
   492  		log.Fatal("Error: must be a git repo or a VERSION file existed.")
   493  	}
   494  	version := getBuildVer() // Closet tag on git log
   495  	return version
   496  }
   497  
   498  // releaseNewVersion tags the repo with provided new tag, and writes new tag into VERSION file.
   499  func releaseNewVersion(tag string) {
   500  	if !isGitRepo() {
   501  		log.Fatalln("Error: Releasing a new version could only be operated under a git repo.")
   502  	}
   503  	gitRemote.CheckRemoteUrl()
   504  	if getTagRev(tag) != "" {
   505  		log.Fatalln("Error: tag already exists -", tag)
   506  	}
   507  
   508  	version := tag
   509  	re := regexp.MustCompile(`^v\d+?\.\d+?`)
   510  	releaseBranch := re.FindString(version)
   511  	if releaseBranch == "" {
   512  		log.Fatal("Error: A valid version should be has form: vX.Y.Z")
   513  	}
   514  
   515  	sourceBranch := getGitBranch()
   516  
   517  	// Checkout to release breanch
   518  	if sourceBranch != releaseBranch {
   519  		if err := gitCheckoutBranch(releaseBranch); err != nil {
   520  			log.Fatalf("Error: checkout to release branch: %s failed with error: %v.", releaseBranch, err)
   521  		}
   522  		defer func() {
   523  			// Checkout back to source branch
   524  			if err := gitCheckoutBranch(sourceBranch); err != nil {
   525  				log.Fatalf("Error: checkout to source branch: %s failed with error: %v.", sourceBranch, err)
   526  			}
   527  			gitRemote.DeleteBranch(releaseBranch)
   528  		}()
   529  	}
   530  
   531  	// Cache new version
   532  	versionFile := filepath.Join(gopRoot, "VERSION")
   533  	if err := os.WriteFile(versionFile, []byte(version), 0644); err != nil {
   534  		log.Fatalf("Error: cache new version with error: %v\n", err)
   535  	}
   536  
   537  	// Commit changes
   538  	gitAdd(versionFile)
   539  	if err := gitCommit("release version " + version); err != nil {
   540  		log.Fatalf("Error: git commit with error: %v\n", err)
   541  	}
   542  
   543  	// Tag the source code
   544  	if err := gitTagAndPushTo(tag, "gop", releaseBranch); err != nil {
   545  		log.Fatalf("Error: gitTagAndPushTo with error: %v\n", err)
   546  	}
   547  
   548  	println("Released new version:", version)
   549  }
   550  
   551  func runRegtests() {
   552  	println("\nStart running regtests.")
   553  
   554  	cmd := exec.Command(filepath.Join(gopRoot, "bin/"+gopBinFiles[0]), "go", "./...")
   555  	cmd.Stdout = os.Stdout
   556  	cmd.Stderr = os.Stderr
   557  	cmd.Dir = filepath.Join(gopRoot, "testdata")
   558  	err := cmd.Run()
   559  	if err != nil {
   560  		code := cmd.ProcessState.ExitCode()
   561  		if code == 0 {
   562  			code = 1
   563  		}
   564  		os.Exit(code)
   565  	}
   566  }
   567  
   568  func main() {
   569  	isInstall := flag.Bool("install", false, "Install Go+")
   570  	isBuild := flag.Bool("build", false, "Build Go+ tools")
   571  	isTest := flag.Bool("test", false, "Run testcases")
   572  	isRegtest := flag.Bool("regtest", false, "Run regtests")
   573  	isUninstall := flag.Bool("uninstall", false, "Uninstall Go+")
   574  	isGoProxy := flag.Bool("proxy", false, "Set GOPROXY for people in China")
   575  	isAutoProxy := flag.Bool("autoproxy", false, "Check to set GOPROXY automatically")
   576  	noPush := flag.Bool("nopush", false, "Don't push to remote repo")
   577  	tag := flag.String("tag", "", "Release an new version with specified tag")
   578  
   579  	flag.Parse()
   580  
   581  	useGoProxy := *isGoProxy
   582  	if !useGoProxy && *isAutoProxy {
   583  		useGoProxy = isInChina()
   584  	}
   585  	flagActionMap := map[*bool]func(){
   586  		isBuild: func() { buildGoplusTools(useGoProxy) },
   587  		isInstall: func() {
   588  			buildGoplusTools(useGoProxy)
   589  			install()
   590  		},
   591  		isUninstall: uninstall,
   592  		isTest:      runTestcases,
   593  		isRegtest:   runRegtests,
   594  	}
   595  
   596  	// Sort flags, for example: install flag should be checked earlier than test flag.
   597  	flags := []*bool{isBuild, isInstall, isTest, isRegtest, isUninstall}
   598  	hasActionDone := false
   599  
   600  	if *tag != "" {
   601  		if *noPush {
   602  			gitRemote = &gitRemoteNone{}
   603  		}
   604  		releaseNewVersion(*tag)
   605  		hasActionDone = true
   606  	}
   607  
   608  	for _, flag := range flags {
   609  		if *flag {
   610  			flagActionMap[flag]()
   611  			hasActionDone = true
   612  		}
   613  	}
   614  
   615  	if !hasActionDone {
   616  		println("Usage:\n")
   617  		flag.PrintDefaults()
   618  	}
   619  }