github.com/mckael/restic@v0.8.3/build.go (about)

     1  // BSD 2-Clause License
     2  //
     3  // Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
     4  // All rights reserved.
     5  //
     6  // This file has been copied from the repository at:
     7  // https://github.com/fd0/build-go
     8  //
     9  // Redistribution and use in source and binary forms, with or without
    10  // modification, are permitted provided that the following conditions are met:
    11  //
    12  // * Redistributions of source code must retain the above copyright notice, this
    13  //   list of conditions and the following disclaimer.
    14  //
    15  // * Redistributions in binary form must reproduce the above copyright notice,
    16  //   this list of conditions and the following disclaimer in the documentation
    17  //   and/or other materials provided with the distribution.
    18  //
    19  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    20  // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    21  // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    22  // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    23  // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    24  // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    25  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    26  // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    27  // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    28  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    29  
    30  // +build ignore_build_go
    31  
    32  package main
    33  
    34  import (
    35  	"fmt"
    36  	"io"
    37  	"io/ioutil"
    38  	"os"
    39  	"os/exec"
    40  	"path"
    41  	"path/filepath"
    42  	"runtime"
    43  	"strconv"
    44  	"strings"
    45  )
    46  
    47  // config contains the configuration for the program to build.
    48  var config = Config{
    49  	Name:      "restic",                              // name of the program executable and directory
    50  	Namespace: "github.com/restic/restic",            // subdir of GOPATH, e.g. "github.com/foo/bar"
    51  	Main:      "github.com/restic/restic/cmd/restic", // package name for the main package
    52  	Tests: []string{ // tests to run
    53  		"github.com/restic/restic/internal/...",
    54  		"github.com/restic/restic/cmd/...",
    55  	},
    56  	MinVersion: GoVersion{Major: 1, Minor: 8, Patch: 0}, // minimum Go version supported
    57  }
    58  
    59  // Config configures the build.
    60  type Config struct {
    61  	Name       string
    62  	Namespace  string
    63  	Main       string
    64  	Tests      []string
    65  	MinVersion GoVersion
    66  }
    67  
    68  var (
    69  	verbose    bool
    70  	keepGopath bool
    71  	runTests   bool
    72  	enableCGO  bool
    73  )
    74  
    75  // specialDir returns true if the file begins with a special character ('.' or '_').
    76  func specialDir(name string) bool {
    77  	if name == "." {
    78  		return false
    79  	}
    80  
    81  	base := filepath.Base(name)
    82  	if base == "vendor" || base[0] == '_' || base[0] == '.' {
    83  		return true
    84  	}
    85  
    86  	return false
    87  }
    88  
    89  // excludePath returns true if the file should not be copied to the new GOPATH.
    90  func excludePath(name string) bool {
    91  	ext := path.Ext(name)
    92  	if ext == ".go" || ext == ".s" || ext == ".h" {
    93  		return false
    94  	}
    95  
    96  	parentDir := filepath.Base(filepath.Dir(name))
    97  	if parentDir == "testdata" {
    98  		return false
    99  	}
   100  
   101  	return true
   102  }
   103  
   104  // updateGopath builds a valid GOPATH at dst, with all Go files in src/ copied
   105  // to dst/prefix/, so calling
   106  //
   107  //   updateGopath("/tmp/gopath", "/home/u/restic", "github.com/restic/restic")
   108  //
   109  // with "/home/u/restic" containing the file "foo.go" yields the following tree
   110  // at "/tmp/gopath":
   111  //
   112  //   /tmp/gopath
   113  //   └── src
   114  //       └── github.com
   115  //           └── restic
   116  //               └── restic
   117  //                   └── foo.go
   118  func updateGopath(dst, src, prefix string) error {
   119  	verbosePrintf("copy contents of %v to %v\n", src, filepath.Join(dst, prefix))
   120  	return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
   121  		if name == src {
   122  			return err
   123  		}
   124  
   125  		if specialDir(name) {
   126  			if fi.IsDir() {
   127  				return filepath.SkipDir
   128  			}
   129  
   130  			return nil
   131  		}
   132  
   133  		if err != nil {
   134  			return err
   135  		}
   136  
   137  		if fi.IsDir() {
   138  			return nil
   139  		}
   140  
   141  		if excludePath(name) {
   142  			return nil
   143  		}
   144  
   145  		intermediatePath, err := filepath.Rel(src, name)
   146  		if err != nil {
   147  			return err
   148  		}
   149  
   150  		fileSrc := filepath.Join(src, intermediatePath)
   151  		fileDst := filepath.Join(dst, "src", prefix, intermediatePath)
   152  
   153  		return copyFile(fileDst, fileSrc)
   154  	})
   155  }
   156  
   157  func directoryExists(dirname string) bool {
   158  	stat, err := os.Stat(dirname)
   159  	if err != nil && os.IsNotExist(err) {
   160  		return false
   161  	}
   162  
   163  	return stat.IsDir()
   164  }
   165  
   166  // copyFile creates dst from src, preserving file attributes and timestamps.
   167  func copyFile(dst, src string) error {
   168  	fi, err := os.Stat(src)
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	fsrc, err := os.Open(src)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
   179  		fmt.Printf("MkdirAll(%v)\n", filepath.Dir(dst))
   180  		return err
   181  	}
   182  
   183  	fdst, err := os.Create(dst)
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	if _, err = io.Copy(fdst, fsrc); err != nil {
   189  		return err
   190  	}
   191  
   192  	if err == nil {
   193  		err = fsrc.Close()
   194  	}
   195  
   196  	if err == nil {
   197  		err = fdst.Close()
   198  	}
   199  
   200  	if err == nil {
   201  		err = os.Chmod(dst, fi.Mode())
   202  	}
   203  
   204  	if err == nil {
   205  		err = os.Chtimes(dst, fi.ModTime(), fi.ModTime())
   206  	}
   207  
   208  	return nil
   209  }
   210  
   211  // die prints the message with fmt.Fprintf() to stderr and exits with an error
   212  // code.
   213  func die(message string, args ...interface{}) {
   214  	fmt.Fprintf(os.Stderr, message, args...)
   215  	os.Exit(1)
   216  }
   217  
   218  func showUsage(output io.Writer) {
   219  	fmt.Fprintf(output, "USAGE: go run build.go OPTIONS\n")
   220  	fmt.Fprintf(output, "\n")
   221  	fmt.Fprintf(output, "OPTIONS:\n")
   222  	fmt.Fprintf(output, "  -v     --verbose       output more messages\n")
   223  	fmt.Fprintf(output, "  -t     --tags          specify additional build tags\n")
   224  	fmt.Fprintf(output, "  -k     --keep-gopath   do not remove the GOPATH after build\n")
   225  	fmt.Fprintf(output, "  -T     --test          run tests\n")
   226  	fmt.Fprintf(output, "  -o     --output        set output file name\n")
   227  	fmt.Fprintf(output, "         --enable-cgo    use CGO to link against libc\n")
   228  	fmt.Fprintf(output, "         --goos value    set GOOS for cross-compilation\n")
   229  	fmt.Fprintf(output, "         --goarch value  set GOARCH for cross-compilation\n")
   230  	fmt.Fprintf(output, "         --goarm value  set GOARM for cross-compilation\n")
   231  }
   232  
   233  func verbosePrintf(message string, args ...interface{}) {
   234  	if !verbose {
   235  		return
   236  	}
   237  
   238  	fmt.Printf("build: "+message, args...)
   239  }
   240  
   241  // cleanEnv returns a clean environment with GOPATH and GOBIN removed (if
   242  // present).
   243  func cleanEnv() (env []string) {
   244  	for _, v := range os.Environ() {
   245  		if strings.HasPrefix(v, "GOPATH=") || strings.HasPrefix(v, "GOBIN=") {
   246  			continue
   247  		}
   248  
   249  		env = append(env, v)
   250  	}
   251  
   252  	return env
   253  }
   254  
   255  // build runs "go build args..." with GOPATH set to gopath.
   256  func build(cwd, goos, goarch, goarm, gopath string, args ...string) error {
   257  	a := []string{"build"}
   258  	a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
   259  	a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
   260  	a = append(a, args...)
   261  	cmd := exec.Command("go", a...)
   262  	cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos)
   263  	if goarm != "" {
   264  		cmd.Env = append(cmd.Env, "GOARM="+goarm)
   265  	}
   266  	if !enableCGO {
   267  		cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
   268  	}
   269  
   270  	cmd.Dir = cwd
   271  	cmd.Stdout = os.Stdout
   272  	cmd.Stderr = os.Stderr
   273  	verbosePrintf("go %s\n", args)
   274  
   275  	return cmd.Run()
   276  }
   277  
   278  // test runs "go test args..." with GOPATH set to gopath.
   279  func test(cwd, gopath string, args ...string) error {
   280  	args = append([]string{"test"}, args...)
   281  	cmd := exec.Command("go", args...)
   282  	cmd.Env = append(cleanEnv(), "GOPATH="+gopath)
   283  	cmd.Dir = cwd
   284  	cmd.Stdout = os.Stdout
   285  	cmd.Stderr = os.Stderr
   286  	verbosePrintf("go %s\n", args)
   287  
   288  	return cmd.Run()
   289  }
   290  
   291  // getVersion returns the version string from the file VERSION in the current
   292  // directory.
   293  func getVersionFromFile() string {
   294  	buf, err := ioutil.ReadFile("VERSION")
   295  	if err != nil {
   296  		verbosePrintf("error reading file VERSION: %v\n", err)
   297  		return ""
   298  	}
   299  
   300  	return strings.TrimSpace(string(buf))
   301  }
   302  
   303  // getVersion returns a version string which is a combination of the contents
   304  // of the file VERSION in the current directory and the version from git (if
   305  // available).
   306  func getVersion() string {
   307  	versionFile := getVersionFromFile()
   308  	versionGit := getVersionFromGit()
   309  
   310  	verbosePrintf("version from file 'VERSION' is %q, version from git %q\n",
   311  		versionFile, versionGit)
   312  
   313  	switch {
   314  	case versionFile == "":
   315  		return versionGit
   316  	case versionGit == "":
   317  		return versionFile
   318  	}
   319  
   320  	return fmt.Sprintf("%s (%s)", versionFile, versionGit)
   321  }
   322  
   323  // getVersionFromGit returns a version string that identifies the currently
   324  // checked out git commit.
   325  func getVersionFromGit() string {
   326  	cmd := exec.Command("git", "describe",
   327  		"--long", "--tags", "--dirty", "--always")
   328  	out, err := cmd.Output()
   329  	if err != nil {
   330  		verbosePrintf("git describe returned error: %v\n", err)
   331  		return ""
   332  	}
   333  
   334  	version := strings.TrimSpace(string(out))
   335  	verbosePrintf("git version is %s\n", version)
   336  	return version
   337  }
   338  
   339  // Constants represents a set of constants that are set in the final binary to
   340  // the given value via compiler flags.
   341  type Constants map[string]string
   342  
   343  // LDFlags returns the string that can be passed to go build's `-ldflags`.
   344  func (cs Constants) LDFlags() string {
   345  	l := make([]string, 0, len(cs))
   346  
   347  	for k, v := range cs {
   348  		l = append(l, fmt.Sprintf(`-X "%s=%s"`, k, v))
   349  	}
   350  
   351  	return strings.Join(l, " ")
   352  }
   353  
   354  // GoVersion is the version of Go used to compile the project.
   355  type GoVersion struct {
   356  	Major int
   357  	Minor int
   358  	Patch int
   359  }
   360  
   361  // ParseGoVersion parses the Go version s. If s cannot be parsed, the returned GoVersion is null.
   362  func ParseGoVersion(s string) (v GoVersion) {
   363  	if !strings.HasPrefix(s, "go") {
   364  		return
   365  	}
   366  
   367  	s = s[2:]
   368  	data := strings.Split(s, ".")
   369  	if len(data) != 3 {
   370  		return
   371  	}
   372  
   373  	major, err := strconv.Atoi(data[0])
   374  	if err != nil {
   375  		return
   376  	}
   377  
   378  	minor, err := strconv.Atoi(data[1])
   379  	if err != nil {
   380  		return
   381  	}
   382  
   383  	patch, err := strconv.Atoi(data[2])
   384  	if err != nil {
   385  		return
   386  	}
   387  
   388  	v = GoVersion{
   389  		Major: major,
   390  		Minor: minor,
   391  		Patch: patch,
   392  	}
   393  	return
   394  }
   395  
   396  // AtLeast returns true if v is at least as new as other. If v is empty, true is returned.
   397  func (v GoVersion) AtLeast(other GoVersion) bool {
   398  	var empty GoVersion
   399  
   400  	// the empty version satisfies all versions
   401  	if v == empty {
   402  		return true
   403  	}
   404  
   405  	if v.Major < other.Major {
   406  		return false
   407  	}
   408  
   409  	if v.Minor < other.Minor {
   410  		return false
   411  	}
   412  
   413  	if v.Patch < other.Patch {
   414  		return false
   415  	}
   416  
   417  	return true
   418  }
   419  
   420  func (v GoVersion) String() string {
   421  	return fmt.Sprintf("Go %d.%d.%d", v.Major, v.Minor, v.Patch)
   422  }
   423  
   424  func main() {
   425  	ver := ParseGoVersion(runtime.Version())
   426  	if !ver.AtLeast(config.MinVersion) {
   427  		fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", ver, config.MinVersion)
   428  		os.Exit(1)
   429  	}
   430  
   431  	buildTags := []string{}
   432  
   433  	skipNext := false
   434  	params := os.Args[1:]
   435  
   436  	targetGOOS := runtime.GOOS
   437  	targetGOARCH := runtime.GOARCH
   438  	targetGOARM := ""
   439  
   440  	var outputFilename string
   441  
   442  	for i, arg := range params {
   443  		if skipNext {
   444  			skipNext = false
   445  			continue
   446  		}
   447  
   448  		switch arg {
   449  		case "-v", "--verbose":
   450  			verbose = true
   451  		case "-k", "--keep-gopath":
   452  			keepGopath = true
   453  		case "-t", "-tags", "--tags":
   454  			if i+1 >= len(params) {
   455  				die("-t given but no tag specified")
   456  			}
   457  			skipNext = true
   458  			buildTags = strings.Split(params[i+1], " ")
   459  		case "-o", "--output":
   460  			skipNext = true
   461  			outputFilename = params[i+1]
   462  		case "-T", "--test":
   463  			runTests = true
   464  		case "--enable-cgo":
   465  			enableCGO = true
   466  		case "--goos":
   467  			skipNext = true
   468  			targetGOOS = params[i+1]
   469  		case "--goarch":
   470  			skipNext = true
   471  			targetGOARCH = params[i+1]
   472  		case "--goarm":
   473  			skipNext = true
   474  			targetGOARM = params[i+1]
   475  		case "-h":
   476  			showUsage(os.Stdout)
   477  			return
   478  		default:
   479  			fmt.Fprintf(os.Stderr, "Error: unknown option %q\n\n", arg)
   480  			showUsage(os.Stderr)
   481  			os.Exit(1)
   482  		}
   483  	}
   484  
   485  	if len(buildTags) == 0 {
   486  		verbosePrintf("adding build-tag release\n")
   487  		buildTags = []string{"release"}
   488  	}
   489  
   490  	for i := range buildTags {
   491  		buildTags[i] = strings.TrimSpace(buildTags[i])
   492  	}
   493  
   494  	verbosePrintf("build tags: %s\n", buildTags)
   495  
   496  	root, err := os.Getwd()
   497  	if err != nil {
   498  		die("Getwd(): %v\n", err)
   499  	}
   500  
   501  	gopath, err := ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
   502  	if err != nil {
   503  		die("TempDir(): %v\n", err)
   504  	}
   505  
   506  	verbosePrintf("create GOPATH at %v\n", gopath)
   507  	if err = updateGopath(gopath, root, config.Namespace); err != nil {
   508  		die("copying files from %v/src to %v/src failed: %v\n", root, gopath, err)
   509  	}
   510  
   511  	vendor := filepath.Join(root, "vendor")
   512  	if directoryExists(vendor) {
   513  		if err = updateGopath(gopath, vendor, filepath.Join(config.Namespace, "vendor")); err != nil {
   514  			die("copying files from %v to %v failed: %v\n", root, gopath, err)
   515  		}
   516  	}
   517  
   518  	defer func() {
   519  		if !keepGopath {
   520  			verbosePrintf("remove %v\n", gopath)
   521  			if err = os.RemoveAll(gopath); err != nil {
   522  				die("remove GOPATH at %s failed: %v\n", err)
   523  			}
   524  		} else {
   525  			verbosePrintf("leaving temporary GOPATH at %v\n", gopath)
   526  		}
   527  	}()
   528  
   529  	if outputFilename == "" {
   530  		outputFilename = config.Name
   531  		if targetGOOS == "windows" {
   532  			outputFilename += ".exe"
   533  		}
   534  	}
   535  
   536  	cwd, err := os.Getwd()
   537  	if err != nil {
   538  		die("Getwd() returned %v\n", err)
   539  	}
   540  	output := outputFilename
   541  	if !filepath.IsAbs(output) {
   542  		output = filepath.Join(cwd, output)
   543  	}
   544  
   545  	version := getVersion()
   546  	constants := Constants{}
   547  	if version != "" {
   548  		constants["main.version"] = version
   549  	}
   550  	ldflags := "-s -w " + constants.LDFlags()
   551  	verbosePrintf("ldflags: %s\n", ldflags)
   552  
   553  	args := []string{
   554  		"-tags", strings.Join(buildTags, " "),
   555  		"-ldflags", ldflags,
   556  		"-o", output, config.Main,
   557  	}
   558  
   559  	err = build(filepath.Join(gopath, "src"), targetGOOS, targetGOARCH, targetGOARM, gopath, args...)
   560  	if err != nil {
   561  		die("build failed: %v\n", err)
   562  	}
   563  
   564  	if runTests {
   565  		verbosePrintf("running tests\n")
   566  
   567  		err = test(cwd, gopath, config.Tests...)
   568  		if err != nil {
   569  			die("running tests failed: %v\n", err)
   570  		}
   571  	}
   572  }