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 }