github.com/fawick/restic@v0.1.1-0.20171126184616-c02923fbfc79/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 }