github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/build.go (about)

     1  // +build ignore
     2  
     3  package main
     4  
     5  import (
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"path"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  )
    16  
    17  var (
    18  	verbose    bool
    19  	keepGopath bool
    20  	runTests   bool
    21  	enableCGO  bool
    22  )
    23  
    24  var config = struct {
    25  	Name      string
    26  	Namespace string
    27  	Main      string
    28  	Tests     []string
    29  }{
    30  	Name:      "restic",                              // name of the program executable and directory
    31  	Namespace: "github.com/restic/restic",            // subdir of GOPATH, e.g. "github.com/foo/bar"
    32  	Main:      "github.com/restic/restic/cmd/restic", // package name for the main package
    33  	Tests: []string{ // tests to run
    34  		"github.com/restic/restic/internal/...",
    35  		"github.com/restic/restic/cmd/..."},
    36  }
    37  
    38  // specialDir returns true if the file begins with a special character ('.' or '_').
    39  func specialDir(name string) bool {
    40  	if name == "." {
    41  		return false
    42  	}
    43  
    44  	base := filepath.Base(name)
    45  	if base == "vendor" || base[0] == '_' || base[0] == '.' {
    46  		return true
    47  	}
    48  
    49  	return false
    50  }
    51  
    52  // excludePath returns true if the file should not be copied to the new GOPATH.
    53  func excludePath(name string) bool {
    54  	ext := path.Ext(name)
    55  	if ext == ".go" || ext == ".s" || ext == ".h" {
    56  		return false
    57  	}
    58  
    59  	parentDir := filepath.Base(filepath.Dir(name))
    60  	if parentDir == "testdata" {
    61  		return false
    62  	}
    63  
    64  	return true
    65  }
    66  
    67  // updateGopath builds a valid GOPATH at dst, with all Go files in src/ copied
    68  // to dst/prefix/, so calling
    69  //
    70  //   updateGopath("/tmp/gopath", "/home/u/restic", "github.com/restic/restic")
    71  //
    72  // with "/home/u/restic" containing the file "foo.go" yields the following tree
    73  // at "/tmp/gopath":
    74  //
    75  //   /tmp/gopath
    76  //   └── src
    77  //       └── github.com
    78  //           └── restic
    79  //               └── restic
    80  //                   └── foo.go
    81  func updateGopath(dst, src, prefix string) error {
    82  	verbosePrintf("copy contents of %v to %v\n", src, filepath.Join(dst, prefix))
    83  	return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
    84  		if name == src {
    85  			return err
    86  		}
    87  
    88  		if specialDir(name) {
    89  			if fi.IsDir() {
    90  				return filepath.SkipDir
    91  			}
    92  
    93  			return nil
    94  		}
    95  
    96  		if err != nil {
    97  			return err
    98  		}
    99  
   100  		if fi.IsDir() {
   101  			return nil
   102  		}
   103  
   104  		if excludePath(name) {
   105  			return nil
   106  		}
   107  
   108  		intermediatePath, err := filepath.Rel(src, name)
   109  		if err != nil {
   110  			return err
   111  		}
   112  
   113  		fileSrc := filepath.Join(src, intermediatePath)
   114  		fileDst := filepath.Join(dst, "src", prefix, intermediatePath)
   115  
   116  		return copyFile(fileDst, fileSrc)
   117  	})
   118  }
   119  
   120  func directoryExists(dirname string) bool {
   121  	stat, err := os.Stat(dirname)
   122  	if err != nil && os.IsNotExist(err) {
   123  		return false
   124  	}
   125  
   126  	return stat.IsDir()
   127  }
   128  
   129  // copyFile creates dst from src, preserving file attributes and timestamps.
   130  func copyFile(dst, src string) error {
   131  	fi, err := os.Stat(src)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	fsrc, err := os.Open(src)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	defer fsrc.Close()
   141  
   142  	if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
   143  		fmt.Printf("MkdirAll(%v)\n", filepath.Dir(dst))
   144  		return err
   145  	}
   146  
   147  	fdst, err := os.Create(dst)
   148  	if err != nil {
   149  		return err
   150  	}
   151  	defer fdst.Close()
   152  
   153  	_, err = io.Copy(fdst, fsrc)
   154  	if err == nil {
   155  		err = os.Chmod(dst, fi.Mode())
   156  	}
   157  	if err == nil {
   158  		err = os.Chtimes(dst, fi.ModTime(), fi.ModTime())
   159  	}
   160  
   161  	return err
   162  }
   163  
   164  // die prints the message with fmt.Fprintf() to stderr and exits with an error
   165  // code.
   166  func die(message string, args ...interface{}) {
   167  	fmt.Fprintf(os.Stderr, message, args...)
   168  	os.Exit(1)
   169  }
   170  
   171  func showUsage(output io.Writer) {
   172  	fmt.Fprintf(output, "USAGE: go run build.go OPTIONS\n")
   173  	fmt.Fprintf(output, "\n")
   174  	fmt.Fprintf(output, "OPTIONS:\n")
   175  	fmt.Fprintf(output, "  -v     --verbose       output more messages\n")
   176  	fmt.Fprintf(output, "  -t     --tags          specify additional build tags\n")
   177  	fmt.Fprintf(output, "  -k     --keep-gopath   do not remove the GOPATH after build\n")
   178  	fmt.Fprintf(output, "  -T     --test          run tests\n")
   179  	fmt.Fprintf(output, "  -o     --output        set output file name\n")
   180  	fmt.Fprintf(output, "         --enable-cgo    use CGO to link against libc\n")
   181  	fmt.Fprintf(output, "         --goos value    set GOOS for cross-compilation\n")
   182  	fmt.Fprintf(output, "         --goarch value  set GOARCH for cross-compilation\n")
   183  }
   184  
   185  func verbosePrintf(message string, args ...interface{}) {
   186  	if !verbose {
   187  		return
   188  	}
   189  
   190  	fmt.Printf("build: "+message, args...)
   191  }
   192  
   193  // cleanEnv returns a clean environment with GOPATH and GOBIN removed (if
   194  // present).
   195  func cleanEnv() (env []string) {
   196  	for _, v := range os.Environ() {
   197  		if strings.HasPrefix(v, "GOPATH=") || strings.HasPrefix(v, "GOBIN=") {
   198  			continue
   199  		}
   200  
   201  		env = append(env, v)
   202  	}
   203  
   204  	return env
   205  }
   206  
   207  // build runs "go build args..." with GOPATH set to gopath.
   208  func build(cwd, goos, goarch, gopath string, args ...string) error {
   209  	a := []string{"build"}
   210  	a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
   211  	a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
   212  	a = append(a, args...)
   213  	cmd := exec.Command("go", a...)
   214  	cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos)
   215  	if !enableCGO {
   216  		cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
   217  	}
   218  
   219  	cmd.Dir = cwd
   220  	cmd.Stdout = os.Stdout
   221  	cmd.Stderr = os.Stderr
   222  	verbosePrintf("go %s\n", args)
   223  
   224  	return cmd.Run()
   225  }
   226  
   227  // test runs "go test args..." with GOPATH set to gopath.
   228  func test(cwd, gopath string, args ...string) error {
   229  	args = append([]string{"test"}, args...)
   230  	cmd := exec.Command("go", args...)
   231  	cmd.Env = append(cleanEnv(), "GOPATH="+gopath)
   232  	cmd.Dir = cwd
   233  	cmd.Stdout = os.Stdout
   234  	cmd.Stderr = os.Stderr
   235  	verbosePrintf("go %s\n", args)
   236  
   237  	return cmd.Run()
   238  }
   239  
   240  // getVersion returns the version string from the file VERSION in the current
   241  // directory.
   242  func getVersionFromFile() string {
   243  	buf, err := ioutil.ReadFile("VERSION")
   244  	if err != nil {
   245  		verbosePrintf("error reading file VERSION: %v\n", err)
   246  		return ""
   247  	}
   248  
   249  	return strings.TrimSpace(string(buf))
   250  }
   251  
   252  // getVersion returns a version string which is a combination of the contents
   253  // of the file VERSION in the current directory and the version from git (if
   254  // available).
   255  func getVersion() string {
   256  	versionFile := getVersionFromFile()
   257  	versionGit := getVersionFromGit()
   258  
   259  	verbosePrintf("version from file 'VERSION' is %q, version from git %q\n",
   260  		versionFile, versionGit)
   261  
   262  	switch {
   263  	case versionFile == "":
   264  		return versionGit
   265  	case versionGit == "":
   266  		return versionFile
   267  	}
   268  
   269  	return fmt.Sprintf("%s (%s)", versionFile, versionGit)
   270  }
   271  
   272  // getVersionFromGit returns a version string that identifies the currently
   273  // checked out git commit.
   274  func getVersionFromGit() string {
   275  	cmd := exec.Command("git", "describe",
   276  		"--long", "--tags", "--dirty", "--always")
   277  	out, err := cmd.Output()
   278  	if err != nil {
   279  		verbosePrintf("git describe returned error: %v\n", err)
   280  		return ""
   281  	}
   282  
   283  	version := strings.TrimSpace(string(out))
   284  	verbosePrintf("git version is %s\n", version)
   285  	return version
   286  }
   287  
   288  // Constants represents a set of constants that are set in the final binary to
   289  // the given value via compiler flags.
   290  type Constants map[string]string
   291  
   292  // LDFlags returns the string that can be passed to go build's `-ldflags`.
   293  func (cs Constants) LDFlags() string {
   294  	l := make([]string, 0, len(cs))
   295  
   296  	for k, v := range cs {
   297  		l = append(l, fmt.Sprintf(`-X "%s=%s"`, k, v))
   298  	}
   299  
   300  	return strings.Join(l, " ")
   301  }
   302  
   303  func main() {
   304  	ver := runtime.Version()
   305  	if strings.HasPrefix(ver, "go1") && ver < "go1.8" {
   306  		fmt.Fprintf(os.Stderr, "Go version %s detected, restic requires at least Go 1.8\n", ver)
   307  		os.Exit(1)
   308  	}
   309  
   310  	buildTags := []string{}
   311  
   312  	skipNext := false
   313  	params := os.Args[1:]
   314  
   315  	targetGOOS := runtime.GOOS
   316  	targetGOARCH := runtime.GOARCH
   317  
   318  	var outputFilename string
   319  
   320  	for i, arg := range params {
   321  		if skipNext {
   322  			skipNext = false
   323  			continue
   324  		}
   325  
   326  		switch arg {
   327  		case "-v", "--verbose":
   328  			verbose = true
   329  		case "-k", "--keep-gopath":
   330  			keepGopath = true
   331  		case "-t", "-tags", "--tags":
   332  			if i+1 >= len(params) {
   333  				die("-t given but no tag specified")
   334  			}
   335  			skipNext = true
   336  			buildTags = strings.Split(params[i+1], " ")
   337  		case "-o", "--output":
   338  			skipNext = true
   339  			outputFilename = params[i+1]
   340  		case "-T", "--test":
   341  			runTests = true
   342  		case "--enable-cgo":
   343  			enableCGO = true
   344  		case "--goos":
   345  			skipNext = true
   346  			targetGOOS = params[i+1]
   347  		case "--goarch":
   348  			skipNext = true
   349  			targetGOARCH = params[i+1]
   350  		case "-h":
   351  			showUsage(os.Stdout)
   352  			return
   353  		default:
   354  			fmt.Fprintf(os.Stderr, "Error: unknown option %q\n\n", arg)
   355  			showUsage(os.Stderr)
   356  			os.Exit(1)
   357  		}
   358  	}
   359  
   360  	if len(buildTags) == 0 {
   361  		verbosePrintf("adding build-tag release\n")
   362  		buildTags = []string{"release"}
   363  	}
   364  
   365  	for i := range buildTags {
   366  		buildTags[i] = strings.TrimSpace(buildTags[i])
   367  	}
   368  
   369  	verbosePrintf("build tags: %s\n", buildTags)
   370  
   371  	root, err := os.Getwd()
   372  	if err != nil {
   373  		die("Getwd(): %v\n", err)
   374  	}
   375  
   376  	gopath, err := ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
   377  	if err != nil {
   378  		die("TempDir(): %v\n", err)
   379  	}
   380  
   381  	verbosePrintf("create GOPATH at %v\n", gopath)
   382  	if err = updateGopath(gopath, root, config.Namespace); err != nil {
   383  		die("copying files from %v/src to %v/src failed: %v\n", root, gopath, err)
   384  	}
   385  
   386  	vendor := filepath.Join(root, "vendor")
   387  	if directoryExists(vendor) {
   388  		if err = updateGopath(gopath, vendor, filepath.Join(config.Namespace, "vendor")); err != nil {
   389  			die("copying files from %v to %v failed: %v\n", root, gopath, err)
   390  		}
   391  	}
   392  
   393  	defer func() {
   394  		if !keepGopath {
   395  			verbosePrintf("remove %v\n", gopath)
   396  			if err = os.RemoveAll(gopath); err != nil {
   397  				die("remove GOPATH at %s failed: %v\n", err)
   398  			}
   399  		} else {
   400  			verbosePrintf("leaving temporary GOPATH at %v\n", gopath)
   401  		}
   402  	}()
   403  
   404  	if outputFilename == "" {
   405  		outputFilename = config.Name
   406  		if targetGOOS == "windows" {
   407  			outputFilename += ".exe"
   408  		}
   409  	}
   410  
   411  	cwd, err := os.Getwd()
   412  	if err != nil {
   413  		die("Getwd() returned %v\n", err)
   414  	}
   415  	output := outputFilename
   416  	if !filepath.IsAbs(output) {
   417  		output = filepath.Join(cwd, output)
   418  	}
   419  
   420  	version := getVersion()
   421  	constants := Constants{}
   422  	if version != "" {
   423  		constants["main.version"] = version
   424  	}
   425  	ldflags := "-s -w " + constants.LDFlags()
   426  	verbosePrintf("ldflags: %s\n", ldflags)
   427  
   428  	args := []string{
   429  		"-tags", strings.Join(buildTags, " "),
   430  		"-ldflags", ldflags,
   431  		"-o", output, config.Main,
   432  	}
   433  
   434  	err = build(filepath.Join(gopath, "src"), targetGOOS, targetGOARCH, gopath, args...)
   435  	if err != nil {
   436  		die("build failed: %v\n", err)
   437  	}
   438  
   439  	if runTests {
   440  		verbosePrintf("running tests\n")
   441  
   442  		err = test(cwd, gopath, config.Tests...)
   443  		if err != nil {
   444  			die("running tests failed: %v\n", err)
   445  		}
   446  	}
   447  }