├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.go └── version_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - "1.9.x" 6 | - "1.10.x" 7 | - tip 8 | 9 | matrix: 10 | allow_failures: 11 | - go: tip 12 | 13 | install: 14 | - go version 15 | - export GOBIN="$GOPATH/bin" 16 | - export PATH="$PATH:$GOBIN" 17 | - go get -u github.com/golang/lint/golint 18 | - go get golang.org/x/tools/cmd/goimports 19 | - go get honnef.co/go/tools/... 20 | 21 | script: 22 | - go test -v -tags ignore_build_go 23 | - diff <(goimports -d *.go) <(printf "") 24 | - diff <(unused .) <(printf "") 25 | - diff <(gosimple .) <(printf "") 26 | - diff <(staticcheck .) <(printf "") 27 | 28 | after_success: 29 | - diff <(golint ./...) <(printf "") 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2016-2018, Alexander Neumann 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains a small and self-contained Go program called 2 | `build.go`. It used to compile a binary (package `main`) from either a checkout 3 | of the repository, or from an extracted release tar file. This enables 4 | end-users to compile the program without having to setup a `GOPATH`. 5 | 6 | For it to function correctly, all dependencies need to be vendored, e.g. with 7 | `dep` or `go mod vendor`. Then, your build does not depend on any third-party 8 | resources on the Internet. For Go >= 1.11, modules are used for building and 9 | `GOPROXY` is set to `off` so that no Internet is needed when building. 10 | 11 | The program has a build tag that is not set normally (`ignore_build_go`) so it 12 | is not considered when compiling the other Go code in a repository. 13 | 14 | Usage 15 | ===== 16 | 17 | In order to use it, copy `build.go` to the root level into your repository and 18 | edit the configuration section at the top. You can see an example in the 19 | [restic repository](https://github.com/restic/restic/blob/master/build.go). 20 | 21 | Instruct your users to call `go run build.go` and it will produce a binary from 22 | either a checkout of the repository or from an extracted release tar file. For 23 | Go 1.11, it needs to be called as `go run -mod=vendor build.go` so that no 24 | network access is needed. 25 | 26 | For cross-compilation, the options `--goos` and `--goarch` can be used, e.g. 27 | like this: 28 | ``` 29 | $ go run build.go --goos windows --goarch 386 30 | ``` 31 | 32 | The tests can be run by specifying `--test`. By default, `cgo` is explicitly 33 | disabled by passing `CGO_ENABLED=0` to `go build`, it can be re-enabled 34 | manually by running `go run build.go --enable-cgo`. 35 | 36 | The program will set the string variable `version` in package `main` to a 37 | version string consisting of the contents of the file `VERSION` (if present) 38 | and the output of `git describe` (if available). 39 | 40 | The version string can then be used e.g. in a `version` command, like with 41 | restic: 42 | ``` 43 | $ ./restic version 44 | restic 0.8.1 (v0.8.1-154-g74665a22) 45 | ``` 46 | 47 | The version string consists of: 48 | * The contents of the `VERSION` file: `0.8.1` 49 | * The nearest tag (`v0.8.1`), the number of commits (`154`) and the Git commit hash (`74665a22`) 50 | 51 | Background 52 | ========== 53 | 54 | The program `build.go` constructs a temporary `GOPATH` (for Go < 1.11, or when 55 | no `go.mod` exists) in a temporary directory as configured at the beginning of 56 | the program, then calls `go build` using the temporary `GOPATH`. This means 57 | that end-users do not have to setup a `GOPATH` of their own. 58 | 59 | Testing 60 | ======= 61 | 62 | The tests need to be run with the build tag `ignore_build_go` set: 63 | ``` 64 | $ go test -tags ignore_build_go 65 | PASS 66 | ok github.com/fd0/build-go 0.001s 67 | ``` 68 | -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | // Description 2 | // 3 | // This program aims to make building Go programs for end users easier by just 4 | // calling it with `go run`, without having to setup a GOPATH. 5 | // 6 | // For Go < 1.11, it'll create a new GOPATH in a temporary directory, then run 7 | // `go build` on the package configured as Main in the Config struct. 8 | // 9 | // For Go >= 1.11 if the file go.mod is present, it'll use Go modules and not 10 | // setup a GOPATH. It builds the package configured as Main in the Config 11 | // struct with `go build -mod=vendor` to use the vendored dependencies. 12 | // The variable GOPROXY is set to `off` so that no network calls are made. All 13 | // files are copied to a temporary directory before `go build` is called within 14 | // that directory. 15 | 16 | // BSD 2-Clause License 17 | // 18 | // Copyright (c) 2016-2018, Alexander Neumann 19 | // All rights reserved. 20 | // 21 | // This file has been copied from the repository at: 22 | // https://github.com/fd0/build-go 23 | // 24 | // Redistribution and use in source and binary forms, with or without 25 | // modification, are permitted provided that the following conditions are met: 26 | // 27 | // * Redistributions of source code must retain the above copyright notice, this 28 | // list of conditions and the following disclaimer. 29 | // 30 | // * Redistributions in binary form must reproduce the above copyright notice, 31 | // this list of conditions and the following disclaimer in the documentation 32 | // and/or other materials provided with the distribution. 33 | // 34 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 35 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 36 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 37 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 38 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 39 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 40 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 41 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 42 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 43 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 44 | 45 | // +build ignore_build_go 46 | 47 | package main 48 | 49 | import ( 50 | "fmt" 51 | "io" 52 | "io/ioutil" 53 | "os" 54 | "os/exec" 55 | "path/filepath" 56 | "runtime" 57 | "strconv" 58 | "strings" 59 | ) 60 | 61 | // config contains the configuration for the program to build. 62 | var config = Config{ 63 | Name: "restic", // name of the program executable and directory 64 | Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar" 65 | Main: "./cmd/restic", // package name for the main package 66 | DefaultBuildTags: []string{"selfupdate"}, // specify build tags which are always used 67 | Tests: []string{"./..."}, // tests to run 68 | MinVersion: GoVersion{Major: 1, Minor: 9, Patch: 0}, // minimum Go version supported 69 | } 70 | 71 | // Config configures the build. 72 | type Config struct { 73 | Name string 74 | Namespace string 75 | Main string 76 | DefaultBuildTags []string 77 | Tests []string 78 | MinVersion GoVersion 79 | } 80 | 81 | var ( 82 | verbose bool 83 | keepGopath bool 84 | runTests bool 85 | enableCGO bool 86 | enablePIE bool 87 | goVersion = ParseGoVersion(runtime.Version()) 88 | ) 89 | 90 | // copy all Go files in src to dst, creating directories on the fly, so calling 91 | // 92 | // copy("/tmp/gopath/src/github.com/restic/restic", "/home/u/restic") 93 | // 94 | // with "/home/u/restic" containing the file "foo.go" yields the following tree 95 | // at "/tmp/gopath": 96 | // 97 | // /tmp/gopath 98 | // └── src 99 | // └── github.com 100 | // └── restic 101 | // └── restic 102 | // └── foo.go 103 | func copy(dst, src string) error { 104 | verbosePrintf("copy contents of %v to %v\n", src, dst) 105 | return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error { 106 | if name == src { 107 | return err 108 | } 109 | 110 | if name == ".git" { 111 | return filepath.SkipDir 112 | } 113 | 114 | if err != nil { 115 | return err 116 | } 117 | 118 | if fi.IsDir() { 119 | return nil 120 | } 121 | 122 | intermediatePath, err := filepath.Rel(src, name) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | fileSrc := filepath.Join(src, intermediatePath) 128 | fileDst := filepath.Join(dst, intermediatePath) 129 | 130 | return copyFile(fileDst, fileSrc) 131 | }) 132 | } 133 | 134 | func directoryExists(dirname string) bool { 135 | stat, err := os.Stat(dirname) 136 | if err != nil && os.IsNotExist(err) { 137 | return false 138 | } 139 | 140 | return stat.IsDir() 141 | } 142 | 143 | func fileExists(filename string) bool { 144 | stat, err := os.Stat(filename) 145 | if err != nil && os.IsNotExist(err) { 146 | return false 147 | } 148 | 149 | return stat.Mode().IsRegular() 150 | } 151 | 152 | // copyFile creates dst from src, preserving file attributes and timestamps. 153 | func copyFile(dst, src string) error { 154 | fi, err := os.Stat(src) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | fsrc, err := os.Open(src) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil { 165 | fmt.Printf("MkdirAll(%v)\n", filepath.Dir(dst)) 166 | return err 167 | } 168 | 169 | fdst, err := os.Create(dst) 170 | if err != nil { 171 | _ = fsrc.Close() 172 | return err 173 | } 174 | 175 | _, err = io.Copy(fdst, fsrc) 176 | if err != nil { 177 | _ = fsrc.Close() 178 | _ = fdst.Close() 179 | return err 180 | } 181 | 182 | err = fdst.Close() 183 | if err != nil { 184 | _ = fsrc.Close() 185 | return err 186 | } 187 | 188 | err = fsrc.Close() 189 | if err != nil { 190 | return err 191 | } 192 | 193 | err = os.Chmod(dst, fi.Mode()) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | return os.Chtimes(dst, fi.ModTime(), fi.ModTime()) 199 | } 200 | 201 | // die prints the message with fmt.Fprintf() to stderr and exits with an error 202 | // code. 203 | func die(message string, args ...interface{}) { 204 | fmt.Fprintf(os.Stderr, message, args...) 205 | os.Exit(1) 206 | } 207 | 208 | func showUsage(output io.Writer) { 209 | fmt.Fprintf(output, "USAGE: go run build.go OPTIONS\n") 210 | fmt.Fprintf(output, "\n") 211 | fmt.Fprintf(output, "OPTIONS:\n") 212 | fmt.Fprintf(output, " -v --verbose output more messages\n") 213 | fmt.Fprintf(output, " -t --tags specify additional build tags\n") 214 | fmt.Fprintf(output, " -k --keep-tempdir do not remove the temporary directory after build\n") 215 | fmt.Fprintf(output, " -T --test run tests\n") 216 | fmt.Fprintf(output, " -o --output set output file name\n") 217 | fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n") 218 | fmt.Fprintf(output, " --enable-pie use PIE buildmode\n") 219 | fmt.Fprintf(output, " --goos value set GOOS for cross-compilation\n") 220 | fmt.Fprintf(output, " --goarch value set GOARCH for cross-compilation\n") 221 | fmt.Fprintf(output, " --goarm value set GOARM for cross-compilation\n") 222 | fmt.Fprintf(output, " --tempdir dir use a specific directory for compilation\n") 223 | } 224 | 225 | func verbosePrintf(message string, args ...interface{}) { 226 | if !verbose { 227 | return 228 | } 229 | 230 | fmt.Printf("build: "+message, args...) 231 | } 232 | 233 | // cleanEnv returns a clean environment with GOPATH, GOBIN and GO111MODULE 234 | // removed (if present). 235 | func cleanEnv() (env []string) { 236 | removeKeys := map[string]struct{}{ 237 | "GOPATH": struct{}{}, 238 | "GOBIN": struct{}{}, 239 | "GO111MODULE": struct{}{}, 240 | } 241 | 242 | for _, v := range os.Environ() { 243 | data := strings.SplitN(v, "=", 2) 244 | name := data[0] 245 | 246 | if _, ok := removeKeys[name]; ok { 247 | continue 248 | } 249 | 250 | env = append(env, v) 251 | } 252 | 253 | return env 254 | } 255 | 256 | // build runs "go build args..." with GOPATH set to gopath. 257 | func build(cwd string, env map[string]string, args ...string) error { 258 | a := []string{"build"} 259 | 260 | if goVersion.AtLeast(GoVersion{1, 10, 0}) { 261 | verbosePrintf("Go version is at least 1.10, using new syntax for -gcflags\n") 262 | // use new prefix 263 | a = append(a, "-asmflags", fmt.Sprintf("all=-trimpath=%s", cwd)) 264 | a = append(a, "-gcflags", fmt.Sprintf("all=-trimpath=%s", cwd)) 265 | } else { 266 | a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", cwd)) 267 | a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", cwd)) 268 | } 269 | if enablePIE { 270 | a = append(a, "-buildmode=pie") 271 | } 272 | 273 | a = append(a, args...) 274 | cmd := exec.Command("go", a...) 275 | cmd.Env = append(cleanEnv(), "GOPROXY=off") 276 | for k, v := range env { 277 | cmd.Env = append(cmd.Env, k+"="+v) 278 | } 279 | if !enableCGO { 280 | cmd.Env = append(cmd.Env, "CGO_ENABLED=0") 281 | } 282 | 283 | cmd.Dir = cwd 284 | cmd.Stdout = os.Stdout 285 | cmd.Stderr = os.Stderr 286 | 287 | verbosePrintf("chdir %q\n", cwd) 288 | verbosePrintf("go %q\n", a) 289 | 290 | return cmd.Run() 291 | } 292 | 293 | // test runs "go test args..." with GOPATH set to gopath. 294 | func test(cwd string, env map[string]string, args ...string) error { 295 | args = append([]string{"test", "-count", "1"}, args...) 296 | cmd := exec.Command("go", args...) 297 | cmd.Env = append(cleanEnv(), "GOPROXY=off") 298 | for k, v := range env { 299 | cmd.Env = append(cmd.Env, k+"="+v) 300 | } 301 | if !enableCGO { 302 | cmd.Env = append(cmd.Env, "CGO_ENABLED=0") 303 | } 304 | cmd.Dir = cwd 305 | cmd.Stdout = os.Stdout 306 | cmd.Stderr = os.Stderr 307 | 308 | verbosePrintf("chdir %q\n", cwd) 309 | verbosePrintf("go %q\n", args) 310 | 311 | return cmd.Run() 312 | } 313 | 314 | // getVersion returns the version string from the file VERSION in the current 315 | // directory. 316 | func getVersionFromFile() string { 317 | buf, err := ioutil.ReadFile("VERSION") 318 | if err != nil { 319 | verbosePrintf("error reading file VERSION: %v\n", err) 320 | return "" 321 | } 322 | 323 | return strings.TrimSpace(string(buf)) 324 | } 325 | 326 | // getVersion returns a version string which is a combination of the contents 327 | // of the file VERSION in the current directory and the version from git (if 328 | // available). 329 | func getVersion() string { 330 | versionFile := getVersionFromFile() 331 | versionGit := getVersionFromGit() 332 | 333 | verbosePrintf("version from file 'VERSION' is %q, version from git %q\n", 334 | versionFile, versionGit) 335 | 336 | switch { 337 | case versionFile == "": 338 | return versionGit 339 | case versionGit == "": 340 | return versionFile 341 | } 342 | 343 | return fmt.Sprintf("%s (%s)", versionFile, versionGit) 344 | } 345 | 346 | // getVersionFromGit returns a version string that identifies the currently 347 | // checked out git commit. 348 | func getVersionFromGit() string { 349 | cmd := exec.Command("git", "describe", 350 | "--long", "--tags", "--dirty", "--always") 351 | out, err := cmd.Output() 352 | if err != nil { 353 | verbosePrintf("git describe returned error: %v\n", err) 354 | return "" 355 | } 356 | 357 | version := strings.TrimSpace(string(out)) 358 | verbosePrintf("git version is %s\n", version) 359 | return version 360 | } 361 | 362 | // Constants represents a set of constants that are set in the final binary to 363 | // the given value via compiler flags. 364 | type Constants map[string]string 365 | 366 | // LDFlags returns the string that can be passed to go build's `-ldflags`. 367 | func (cs Constants) LDFlags() string { 368 | l := make([]string, 0, len(cs)) 369 | 370 | for k, v := range cs { 371 | l = append(l, fmt.Sprintf(`-X "%s=%s"`, k, v)) 372 | } 373 | 374 | return strings.Join(l, " ") 375 | } 376 | 377 | // GoVersion is the version of Go used to compile the project. 378 | type GoVersion struct { 379 | Major int 380 | Minor int 381 | Patch int 382 | } 383 | 384 | // ParseGoVersion parses the Go version s. If s cannot be parsed, the returned GoVersion is null. 385 | func ParseGoVersion(s string) (v GoVersion) { 386 | if !strings.HasPrefix(s, "go") { 387 | return 388 | } 389 | 390 | s = s[2:] 391 | data := strings.Split(s, ".") 392 | if len(data) < 2 || len(data) > 3 { 393 | // invalid version 394 | return GoVersion{} 395 | } 396 | 397 | var err error 398 | 399 | v.Major, err = strconv.Atoi(data[0]) 400 | if err != nil { 401 | return GoVersion{} 402 | } 403 | 404 | // try to parse the minor version while removing an eventual suffix (like 405 | // "rc2" or so) 406 | for s := data[1]; s != ""; s = s[:len(s)-1] { 407 | v.Minor, err = strconv.Atoi(s) 408 | if err == nil { 409 | break 410 | } 411 | } 412 | 413 | if v.Minor == 0 { 414 | // no minor version found 415 | return GoVersion{} 416 | } 417 | 418 | if len(data) >= 3 { 419 | v.Patch, err = strconv.Atoi(data[2]) 420 | if err != nil { 421 | return GoVersion{} 422 | } 423 | } 424 | 425 | return 426 | } 427 | 428 | // AtLeast returns true if v is at least as new as other. If v is empty, true is returned. 429 | func (v GoVersion) AtLeast(other GoVersion) bool { 430 | var empty GoVersion 431 | 432 | // the empty version satisfies all versions 433 | if v == empty { 434 | return true 435 | } 436 | 437 | if v.Major < other.Major { 438 | return false 439 | } 440 | 441 | if v.Minor < other.Minor { 442 | return false 443 | } 444 | 445 | if v.Patch < other.Patch { 446 | return false 447 | } 448 | 449 | return true 450 | } 451 | 452 | func (v GoVersion) String() string { 453 | return fmt.Sprintf("Go %d.%d.%d", v.Major, v.Minor, v.Patch) 454 | } 455 | 456 | func main() { 457 | if !goVersion.AtLeast(config.MinVersion) { 458 | fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion) 459 | os.Exit(1) 460 | } 461 | 462 | buildTags := config.DefaultBuildTags 463 | 464 | skipNext := false 465 | params := os.Args[1:] 466 | 467 | goEnv := map[string]string{} 468 | buildEnv := map[string]string{ 469 | "GOOS": runtime.GOOS, 470 | "GOARCH": runtime.GOARCH, 471 | "GOARM": "", 472 | } 473 | 474 | tempdir := "" 475 | 476 | var outputFilename string 477 | 478 | for i, arg := range params { 479 | if skipNext { 480 | skipNext = false 481 | continue 482 | } 483 | 484 | switch arg { 485 | case "-v", "--verbose": 486 | verbose = true 487 | case "-k", "--keep-gopath": 488 | keepGopath = true 489 | case "-t", "-tags", "--tags": 490 | if i+1 >= len(params) { 491 | die("-t given but no tag specified") 492 | } 493 | skipNext = true 494 | buildTags = append(buildTags, strings.Split(params[i+1], " ")...) 495 | case "-o", "--output": 496 | skipNext = true 497 | outputFilename = params[i+1] 498 | case "--tempdir": 499 | skipNext = true 500 | tempdir = params[i+1] 501 | case "-T", "--test": 502 | runTests = true 503 | case "--enable-cgo": 504 | enableCGO = true 505 | case "--enable-pie": 506 | enablePIE = true 507 | case "--goos": 508 | skipNext = true 509 | buildEnv["GOOS"] = params[i+1] 510 | case "--goarch": 511 | skipNext = true 512 | buildEnv["GOARCH"] = params[i+1] 513 | case "--goarm": 514 | skipNext = true 515 | buildEnv["GOARM"] = params[i+1] 516 | case "-h": 517 | showUsage(os.Stdout) 518 | return 519 | default: 520 | fmt.Fprintf(os.Stderr, "Error: unknown option %q\n\n", arg) 521 | showUsage(os.Stderr) 522 | os.Exit(1) 523 | } 524 | } 525 | 526 | verbosePrintf("detected Go version %v\n", goVersion) 527 | 528 | for i := range buildTags { 529 | buildTags[i] = strings.TrimSpace(buildTags[i]) 530 | } 531 | 532 | verbosePrintf("build tags: %s\n", buildTags) 533 | 534 | root, err := os.Getwd() 535 | if err != nil { 536 | die("Getwd(): %v\n", err) 537 | } 538 | 539 | if outputFilename == "" { 540 | outputFilename = config.Name 541 | if buildEnv["GOOS"] == "windows" { 542 | outputFilename += ".exe" 543 | } 544 | } 545 | 546 | output := outputFilename 547 | if !filepath.IsAbs(output) { 548 | output = filepath.Join(root, output) 549 | } 550 | 551 | version := getVersion() 552 | constants := Constants{} 553 | if version != "" { 554 | constants["main.version"] = version 555 | } 556 | ldflags := "-s -w " + constants.LDFlags() 557 | verbosePrintf("ldflags: %s\n", ldflags) 558 | 559 | var ( 560 | buildArgs []string 561 | testArgs []string 562 | ) 563 | 564 | mainPackage := config.Main 565 | if strings.HasPrefix(mainPackage, config.Namespace) { 566 | mainPackage = strings.Replace(mainPackage, config.Namespace, "./", 1) 567 | } 568 | 569 | buildTarget := filepath.FromSlash(mainPackage) 570 | buildCWD := "" 571 | 572 | if goVersion.AtLeast(GoVersion{1, 11, 0}) && fileExists("go.mod") { 573 | verbosePrintf("Go >= 1.11 and 'go.mod' found, building with modules\n") 574 | buildCWD = root 575 | 576 | buildArgs = append(buildArgs, "-mod=vendor") 577 | testArgs = append(testArgs, "-mod=vendor") 578 | 579 | goEnv["GO111MODULE"] = "on" 580 | buildEnv["GO111MODULE"] = "on" 581 | } else { 582 | if tempdir == "" { 583 | tempdir, err = ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name)) 584 | if err != nil { 585 | die("TempDir(): %v\n", err) 586 | } 587 | } 588 | 589 | verbosePrintf("Go < 1.11 or 'go.mod' not found, create GOPATH at %v\n", tempdir) 590 | targetdir := filepath.Join(tempdir, "src", filepath.FromSlash(config.Namespace)) 591 | if err = copy(targetdir, root); err != nil { 592 | die("copying files from %v to %v/src failed: %v\n", root, tempdir, err) 593 | } 594 | 595 | defer func() { 596 | if !keepGopath { 597 | verbosePrintf("remove %v\n", tempdir) 598 | if err = os.RemoveAll(tempdir); err != nil { 599 | die("remove GOPATH at %s failed: %v\n", tempdir, err) 600 | } 601 | } else { 602 | verbosePrintf("leaving temporary GOPATH at %v\n", tempdir) 603 | } 604 | }() 605 | 606 | buildCWD = targetdir 607 | 608 | goEnv["GOPATH"] = tempdir 609 | buildEnv["GOPATH"] = tempdir 610 | } 611 | 612 | verbosePrintf("environment:\n go: %v\n build: %v\n", goEnv, buildEnv) 613 | 614 | buildArgs = append(buildArgs, 615 | "-tags", strings.Join(buildTags, " "), 616 | "-ldflags", ldflags, 617 | "-o", output, buildTarget, 618 | ) 619 | 620 | err = build(buildCWD, buildEnv, buildArgs...) 621 | if err != nil { 622 | die("build failed: %v\n", err) 623 | } 624 | 625 | if runTests { 626 | verbosePrintf("running tests\n") 627 | 628 | testArgs = append(testArgs, config.Tests...) 629 | 630 | err = test(buildCWD, goEnv, testArgs...) 631 | if err != nil { 632 | die("running tests failed: %v\n", err) 633 | } 634 | } 635 | } 636 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestParseGoVersion(t *testing.T) { 6 | var tests = []struct { 7 | s string 8 | v GoVersion 9 | }{ 10 | { 11 | s: "go1.5.7", 12 | v: GoVersion{Major: 1, Minor: 5, Patch: 7}, 13 | }, 14 | { 15 | s: "go71.23.44", 16 | v: GoVersion{Major: 71, Minor: 23, Patch: 44}, 17 | }, 18 | { 19 | s: "1.2.3", 20 | }, 21 | { 22 | s: "go1.2", 23 | v: GoVersion{Major: 1, Minor: 2, Patch: 0}, 24 | }, 25 | { 26 | s: "go1.10", 27 | v: GoVersion{Major: 1, Minor: 10, Patch: 0}, 28 | }, 29 | { 30 | s: "go1.10.5", 31 | v: GoVersion{Major: 1, Minor: 10, Patch: 5}, 32 | }, 33 | { 34 | s: "go1.10rc2", 35 | v: GoVersion{Major: 1, Minor: 10, Patch: 0}, 36 | }, 37 | { 38 | s: "go1.xxxrc2", 39 | }, 40 | { 41 | s: "go1.foo.bar", 42 | }, 43 | } 44 | 45 | for _, test := range tests { 46 | t.Run("", func(t *testing.T) { 47 | v := ParseGoVersion(test.s) 48 | if v != test.v { 49 | t.Fatalf("wrong version, wanted %v, got %v", test.v, v) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | func TestGoVersionAtLeast(t *testing.T) { 56 | var tests = []struct { 57 | v1, v2 GoVersion 58 | res bool 59 | }{ 60 | { 61 | v1: GoVersion{Major: 1, Minor: 9, Patch: 2}, 62 | v2: GoVersion{Major: 1, Minor: 0, Patch: 0}, 63 | res: true, 64 | }, 65 | { 66 | v1: GoVersion{Major: 1, Minor: 5, Patch: 7}, 67 | v2: GoVersion{Major: 71, Minor: 23, Patch: 44}, 68 | res: false, 69 | }, 70 | { 71 | v1: GoVersion{}, 72 | v2: GoVersion{Major: 1, Minor: 7, Patch: 2}, 73 | res: true, 74 | }, 75 | } 76 | 77 | for _, test := range tests { 78 | t.Run("", func(t *testing.T) { 79 | res := test.v1.AtLeast(test.v2) 80 | if res != test.res { 81 | t.Fatalf("wrong result, want %v, got %v", test.res, res) 82 | } 83 | }) 84 | } 85 | } 86 | --------------------------------------------------------------------------------