github.com/ethereum/go-ethereum@v1.16.1/build/ci.go (about) 1 // Copyright 2016 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 //go:build none 18 // +build none 19 20 /* 21 The ci command is called from Continuous Integration scripts. 22 23 Usage: go run build/ci.go <command> <command flags/arguments> 24 25 Available commands are: 26 27 lint -- runs certain pre-selected linters 28 check_generate -- verifies that 'go generate' and 'go mod tidy' do not produce changes 29 check_baddeps -- verifies that certain dependencies are avoided 30 31 install [ -arch architecture ] [ -cc compiler ] [ packages... ] -- builds packages and executables 32 test [ -coverage ] [ packages... ] -- runs the tests 33 34 archive [ -arch architecture ] [ -type zip|tar ] [ -signer key-envvar ] [ -signify key-envvar ] [ -upload dest ] -- archives build artifacts 35 importkeys -- imports signing keys from env 36 debsrc [ -signer key-id ] [ -upload dest ] -- creates a debian source package 37 nsis -- creates a Windows NSIS installer 38 purge [ -store blobstore ] [ -days threshold ] -- purges old archives from the blobstore 39 40 For all commands, -n prevents execution of external programs (dry run mode). 41 */ 42 package main 43 44 import ( 45 "bytes" 46 "encoding/base64" 47 "flag" 48 "fmt" 49 "log" 50 "os" 51 "os/exec" 52 "path" 53 "path/filepath" 54 "runtime" 55 "slices" 56 "strings" 57 "time" 58 59 "github.com/cespare/cp" 60 "github.com/ethereum/go-ethereum/crypto/signify" 61 "github.com/ethereum/go-ethereum/internal/build" 62 "github.com/ethereum/go-ethereum/internal/download" 63 "github.com/ethereum/go-ethereum/internal/version" 64 ) 65 66 var ( 67 // Files that end up in the geth*.zip archive. 68 gethArchiveFiles = []string{ 69 "COPYING", 70 executablePath("geth"), 71 } 72 73 // Files that end up in the geth-alltools*.zip archive. 74 allToolsArchiveFiles = []string{ 75 "COPYING", 76 executablePath("abigen"), 77 executablePath("evm"), 78 executablePath("geth"), 79 executablePath("rlpdump"), 80 executablePath("clef"), 81 } 82 83 // A debian package is created for all executables listed here. 84 debExecutables = []debExecutable{ 85 { 86 BinaryName: "abigen", 87 Description: "Source code generator to convert Ethereum contract definitions into easy to use, compile-time type-safe Go packages.", 88 }, 89 { 90 BinaryName: "evm", 91 Description: "Developer utility version of the EVM (Ethereum Virtual Machine) that is capable of running bytecode snippets within a configurable environment and execution mode.", 92 }, 93 { 94 BinaryName: "geth", 95 Description: "Ethereum CLI client.", 96 }, 97 { 98 BinaryName: "rlpdump", 99 Description: "Developer utility tool that prints RLP structures.", 100 }, 101 { 102 BinaryName: "clef", 103 Description: "Ethereum account management tool.", 104 }, 105 } 106 107 // A debian package is created for all executables listed here. 108 debEthereum = debPackage{ 109 Name: "ethereum", 110 Version: version.Semantic, 111 Executables: debExecutables, 112 } 113 114 // Debian meta packages to build and push to Ubuntu PPA 115 debPackages = []debPackage{ 116 debEthereum, 117 } 118 119 // Distros for which packages are created 120 debDistros = []string{ 121 "xenial", // 16.04, EOL: 04/2026 122 "bionic", // 18.04, EOL: 04/2028 123 "focal", // 20.04, EOL: 04/2030 124 "jammy", // 22.04, EOL: 04/2032 125 "noble", // 24.04, EOL: 04/2034 126 "oracular", // 24.10, EOL: 07/2025 127 } 128 129 // This is where the tests should be unpacked. 130 executionSpecTestsDir = "tests/spec-tests" 131 ) 132 133 var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin")) 134 135 func executablePath(name string) string { 136 if runtime.GOOS == "windows" { 137 name += ".exe" 138 } 139 return filepath.Join(GOBIN, name) 140 } 141 142 func main() { 143 log.SetFlags(log.Lshortfile) 144 145 if !build.FileExist(filepath.Join("build", "ci.go")) { 146 log.Fatal("this script must be run from the root of the repository") 147 } 148 if len(os.Args) < 2 { 149 log.Fatal("need subcommand as first argument") 150 } 151 switch os.Args[1] { 152 case "install": 153 doInstall(os.Args[2:]) 154 case "test": 155 doTest(os.Args[2:]) 156 case "lint": 157 doLint(os.Args[2:]) 158 case "check_generate": 159 doCheckGenerate() 160 case "check_baddeps": 161 doCheckBadDeps() 162 case "archive": 163 doArchive(os.Args[2:]) 164 case "dockerx": 165 doDockerBuildx(os.Args[2:]) 166 case "debsrc": 167 doDebianSource(os.Args[2:]) 168 case "nsis": 169 doWindowsInstaller(os.Args[2:]) 170 case "purge": 171 doPurge(os.Args[2:]) 172 case "sanitycheck": 173 doSanityCheck() 174 default: 175 log.Fatal("unknown command ", os.Args[1]) 176 } 177 } 178 179 // Compiling 180 181 func doInstall(cmdline []string) { 182 var ( 183 dlgo = flag.Bool("dlgo", false, "Download Go and build with it") 184 arch = flag.String("arch", "", "Architecture to cross build for") 185 cc = flag.String("cc", "", "C compiler to cross build with") 186 staticlink = flag.Bool("static", false, "Create statically-linked executable") 187 ) 188 flag.CommandLine.Parse(cmdline) 189 env := build.Env() 190 191 // Configure the toolchain. 192 tc := build.GoToolchain{GOARCH: *arch, CC: *cc} 193 if *dlgo { 194 csdb := download.MustLoadChecksums("build/checksums.txt") 195 tc.Root = build.DownloadGo(csdb) 196 } 197 // Disable CLI markdown doc generation in release builds. 198 buildTags := []string{"urfave_cli_no_docs"} 199 200 // Enable linking the CKZG library since we can make it work with additional flags. 201 if env.UbuntuVersion != "trusty" { 202 buildTags = append(buildTags, "ckzg") 203 } 204 205 // Configure the build. 206 gobuild := tc.Go("build", buildFlags(env, *staticlink, buildTags)...) 207 208 // We use -trimpath to avoid leaking local paths into the built executables. 209 gobuild.Args = append(gobuild.Args, "-trimpath") 210 211 // Show packages during build. 212 gobuild.Args = append(gobuild.Args, "-v") 213 214 // Now we choose what we're even building. 215 // Default: collect all 'main' packages in cmd/ and build those. 216 packages := flag.Args() 217 if len(packages) == 0 { 218 packages = build.FindMainPackages("./cmd") 219 } 220 221 // Do the build! 222 for _, pkg := range packages { 223 args := slices.Clone(gobuild.Args) 224 args = append(args, "-o", executablePath(path.Base(pkg))) 225 args = append(args, pkg) 226 build.MustRun(&exec.Cmd{Path: gobuild.Path, Args: args, Env: gobuild.Env}) 227 } 228 } 229 230 // buildFlags returns the go tool flags for building. 231 func buildFlags(env build.Environment, staticLinking bool, buildTags []string) (flags []string) { 232 var ld []string 233 // See https://github.com/golang/go/issues/33772#issuecomment-528176001 234 // We need to set --buildid to the linker here, and also pass --build-id to the 235 // cgo-linker further down. 236 ld = append(ld, "--buildid=none") 237 if env.Commit != "" { 238 ld = append(ld, "-X", "github.com/ethereum/go-ethereum/internal/version.gitCommit="+env.Commit) 239 ld = append(ld, "-X", "github.com/ethereum/go-ethereum/internal/version.gitDate="+env.Date) 240 } 241 // Strip DWARF on darwin. This used to be required for certain things, 242 // and there is no downside to this, so we just keep doing it. 243 if runtime.GOOS == "darwin" { 244 ld = append(ld, "-s") 245 } 246 if runtime.GOOS == "linux" { 247 // Enforce the stacksize to 8M, which is the case on most platforms apart from 248 // alpine Linux. 249 // See https://sourceware.org/binutils/docs-2.23.1/ld/Options.html#Options 250 // regarding the options --build-id=none and --strip-all. It is needed for 251 // reproducible builds; removing references to temporary files in C-land, and 252 // making build-id reproducibly absent. 253 extld := []string{"-Wl,-z,stack-size=0x800000,--build-id=none,--strip-all"} 254 if staticLinking { 255 extld = append(extld, "-static") 256 // Under static linking, use of certain glibc features must be 257 // disabled to avoid shared library dependencies. 258 buildTags = append(buildTags, "osusergo", "netgo") 259 } 260 ld = append(ld, "-extldflags", "'"+strings.Join(extld, " ")+"'") 261 } 262 if len(ld) > 0 { 263 flags = append(flags, "-ldflags", strings.Join(ld, " ")) 264 } 265 if len(buildTags) > 0 { 266 flags = append(flags, "-tags", strings.Join(buildTags, ",")) 267 } 268 return flags 269 } 270 271 // Running The Tests 272 // 273 // "tests" also includes static analysis tools such as vet. 274 275 func doTest(cmdline []string) { 276 var ( 277 dlgo = flag.Bool("dlgo", false, "Download Go and build with it") 278 arch = flag.String("arch", "", "Run tests for given architecture") 279 cc = flag.String("cc", "", "Sets C compiler binary") 280 coverage = flag.Bool("coverage", false, "Whether to record code coverage") 281 verbose = flag.Bool("v", false, "Whether to log verbosely") 282 race = flag.Bool("race", false, "Execute the race detector") 283 short = flag.Bool("short", false, "Pass the 'short'-flag to go test") 284 cachedir = flag.String("cachedir", "./build/cache", "directory for caching downloads") 285 ) 286 flag.CommandLine.Parse(cmdline) 287 288 // Get test fixtures. 289 csdb := download.MustLoadChecksums("build/checksums.txt") 290 downloadSpecTestFixtures(csdb, *cachedir) 291 292 // Configure the toolchain. 293 tc := build.GoToolchain{GOARCH: *arch, CC: *cc} 294 if *dlgo { 295 tc.Root = build.DownloadGo(csdb) 296 } 297 gotest := tc.Go("test") 298 299 // CI needs a bit more time for the statetests (default 45m). 300 gotest.Args = append(gotest.Args, "-timeout=45m") 301 302 // Enable CKZG backend in CI. 303 gotest.Args = append(gotest.Args, "-tags=ckzg") 304 305 // Enable integration-tests 306 gotest.Args = append(gotest.Args, "-tags=integrationtests") 307 308 // Test a single package at a time. CI builders are slow 309 // and some tests run into timeouts under load. 310 gotest.Args = append(gotest.Args, "-p", "1") 311 if *coverage { 312 gotest.Args = append(gotest.Args, "-covermode=atomic", "-cover") 313 } 314 if *verbose { 315 gotest.Args = append(gotest.Args, "-v") 316 } 317 if *race { 318 gotest.Args = append(gotest.Args, "-race") 319 } 320 if *short { 321 gotest.Args = append(gotest.Args, "-short") 322 } 323 324 packages := []string{"./..."} 325 if len(flag.CommandLine.Args()) > 0 { 326 packages = flag.CommandLine.Args() 327 } 328 gotest.Args = append(gotest.Args, packages...) 329 build.MustRun(gotest) 330 } 331 332 // downloadSpecTestFixtures downloads and extracts the execution-spec-tests fixtures. 333 func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string { 334 ext := ".tar.gz" 335 base := "fixtures_develop" 336 archivePath := filepath.Join(cachedir, base+ext) 337 if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { 338 log.Fatal(err) 339 } 340 if err := build.ExtractArchive(archivePath, executionSpecTestsDir); err != nil { 341 log.Fatal(err) 342 } 343 return filepath.Join(cachedir, base) 344 } 345 346 // doCheckTidy assets that the Go modules files are tidied already. 347 func doCheckTidy() { 348 } 349 350 // doCheckGenerate ensures that re-generating generated files does not cause 351 // any mutations in the source file tree. 352 func doCheckGenerate() { 353 var ( 354 cachedir = flag.String("cachedir", "./build/cache", "directory for caching binaries.") 355 tc = new(build.GoToolchain) 356 ) 357 // Compute the origin hashes of all the files 358 var hashes map[string][32]byte 359 360 var err error 361 hashes, err = build.HashFolder(".", []string{"tests/testdata", "build/cache", ".git"}) 362 if err != nil { 363 log.Fatal("Error computing hashes", "err", err) 364 } 365 // Run any go generate steps we might be missing 366 var ( 367 protocPath = downloadProtoc(*cachedir) 368 protocGenGoPath = downloadProtocGenGo(*cachedir) 369 ) 370 c := tc.Go("generate", "./...") 371 pathList := []string{filepath.Join(protocPath, "bin"), protocGenGoPath, os.Getenv("PATH")} 372 c.Env = append(c.Env, "PATH="+strings.Join(pathList, string(os.PathListSeparator))) 373 build.MustRun(c) 374 375 // Check if generate file hashes have changed 376 generated, err := build.HashFolder(".", []string{"tests/testdata", "build/cache", ".git"}) 377 if err != nil { 378 log.Fatalf("Error re-computing hashes: %v", err) 379 } 380 updates := build.DiffHashes(hashes, generated) 381 for _, file := range updates { 382 log.Printf("File changed: %s", file) 383 } 384 if len(updates) != 0 { 385 log.Fatal("One or more generated files were updated by running 'go generate ./...'") 386 } 387 fmt.Println("No stale files detected.") 388 389 // Run go mod tidy check. 390 build.MustRun(tc.Go("mod", "tidy", "-diff")) 391 fmt.Println("No untidy module files detected.") 392 } 393 394 // doCheckBadDeps verifies whether certain unintended dependencies between some 395 // packages leak into the codebase due to a refactor. This is not an exhaustive 396 // list, rather something we build up over time at sensitive places. 397 func doCheckBadDeps() { 398 baddeps := [][2]string{ 399 // Rawdb tends to be a dumping ground for db utils, sometimes leaking the db itself 400 {"github.com/ethereum/go-ethereum/core/rawdb", "github.com/ethereum/go-ethereum/ethdb/leveldb"}, 401 {"github.com/ethereum/go-ethereum/core/rawdb", "github.com/ethereum/go-ethereum/ethdb/pebbledb"}, 402 } 403 tc := new(build.GoToolchain) 404 405 var failed bool 406 for _, rule := range baddeps { 407 out, err := tc.Go("list", "-deps", rule[0]).CombinedOutput() 408 if err != nil { 409 log.Fatalf("Failed to list '%s' dependencies: %v", rule[0], err) 410 } 411 for _, line := range strings.Split(string(out), "\n") { 412 if strings.TrimSpace(line) == rule[1] { 413 log.Printf("Found bad dependency '%s' -> '%s'", rule[0], rule[1]) 414 failed = true 415 } 416 } 417 } 418 if failed { 419 log.Fatalf("Bad dependencies detected.") 420 } 421 fmt.Println("No bad dependencies detected.") 422 } 423 424 // doLint runs golangci-lint on requested packages. 425 func doLint(cmdline []string) { 426 var ( 427 cachedir = flag.String("cachedir", "./build/cache", "directory for caching golangci-lint binary.") 428 ) 429 flag.CommandLine.Parse(cmdline) 430 packages := []string{"./..."} 431 if len(flag.CommandLine.Args()) > 0 { 432 packages = flag.CommandLine.Args() 433 } 434 435 linter := downloadLinter(*cachedir) 436 lflags := []string{"run", "--config", ".golangci.yml"} 437 build.MustRunCommandWithOutput(linter, append(lflags, packages...)...) 438 fmt.Println("You have achieved perfection.") 439 } 440 441 // downloadLinter downloads and unpacks golangci-lint. 442 func downloadLinter(cachedir string) string { 443 csdb := download.MustLoadChecksums("build/checksums.txt") 444 version, err := csdb.FindVersion("golangci") 445 if err != nil { 446 log.Fatal(err) 447 } 448 arch := runtime.GOARCH 449 ext := ".tar.gz" 450 if runtime.GOOS == "windows" { 451 ext = ".zip" 452 } 453 if arch == "arm" { 454 arch += "v" + os.Getenv("GOARM") 455 } 456 base := fmt.Sprintf("golangci-lint-%s-%s-%s", version, runtime.GOOS, arch) 457 archivePath := filepath.Join(cachedir, base+ext) 458 if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { 459 log.Fatal(err) 460 } 461 if err := build.ExtractArchive(archivePath, cachedir); err != nil { 462 log.Fatal(err) 463 } 464 return filepath.Join(cachedir, base, "golangci-lint") 465 } 466 467 // protocArchiveBaseName returns the name of the protoc archive file for 468 // the current system, stripped of version and file suffix. 469 func protocArchiveBaseName() (string, error) { 470 switch runtime.GOOS + "-" + runtime.GOARCH { 471 case "windows-amd64": 472 return "win64", nil 473 case "windows-386": 474 return "win32", nil 475 case "linux-arm64": 476 return "linux-aarch_64", nil 477 case "linux-386": 478 return "linux-x86_32", nil 479 case "linux-amd64": 480 return "linux-x86_64", nil 481 case "darwin-arm64": 482 return "osx-aarch_64", nil 483 case "darwin-amd64": 484 return "osx-x86_64", nil 485 default: 486 return "", fmt.Errorf("no prebuilt release of protoc available for this system (os: %s, arch: %s)", runtime.GOOS, runtime.GOARCH) 487 } 488 } 489 490 // downloadProtocGenGo downloads protoc-gen-go, which is used by protoc 491 // in the generate command. It returns the full path of the directory 492 // containing the 'protoc-gen-go' executable. 493 func downloadProtocGenGo(cachedir string) string { 494 csdb := download.MustLoadChecksums("build/checksums.txt") 495 version, err := csdb.FindVersion("protoc-gen-go") 496 if err != nil { 497 log.Fatal(err) 498 } 499 baseName := fmt.Sprintf("protoc-gen-go.v%s.%s.%s", version, runtime.GOOS, runtime.GOARCH) 500 archiveName := baseName 501 if runtime.GOOS == "windows" { 502 archiveName += ".zip" 503 } else { 504 archiveName += ".tar.gz" 505 } 506 507 archivePath := path.Join(cachedir, archiveName) 508 if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { 509 log.Fatal(err) 510 } 511 extractDest := filepath.Join(cachedir, baseName) 512 if err := build.ExtractArchive(archivePath, extractDest); err != nil { 513 log.Fatal(err) 514 } 515 extractDest, err = filepath.Abs(extractDest) 516 if err != nil { 517 log.Fatal("error resolving absolute path for protoc", "err", err) 518 } 519 return extractDest 520 } 521 522 // downloadProtoc downloads the prebuilt protoc binary used to lint generated 523 // files as a CI step. It returns the full path to the directory containing 524 // the protoc executable. 525 func downloadProtoc(cachedir string) string { 526 csdb := download.MustLoadChecksums("build/checksums.txt") 527 version, err := csdb.FindVersion("protoc") 528 if err != nil { 529 log.Fatal(err) 530 } 531 baseName, err := protocArchiveBaseName() 532 if err != nil { 533 log.Fatal(err) 534 } 535 536 fileName := fmt.Sprintf("protoc-%s-%s", version, baseName) 537 archiveFileName := fileName + ".zip" 538 archivePath := filepath.Join(cachedir, archiveFileName) 539 if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { 540 log.Fatal(err) 541 } 542 extractDest := filepath.Join(cachedir, fileName) 543 if err := build.ExtractArchive(archivePath, extractDest); err != nil { 544 log.Fatal(err) 545 } 546 extractDest, err = filepath.Abs(extractDest) 547 if err != nil { 548 log.Fatal("error resolving absolute path for protoc", "err", err) 549 } 550 return extractDest 551 } 552 553 // Release Packaging 554 func doArchive(cmdline []string) { 555 var ( 556 arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging") 557 atype = flag.String("type", "zip", "Type of archive to write (zip|tar)") 558 signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`) 559 signify = flag.String("signify", "", `Environment variable holding the signify key (e.g. LINUX_SIGNIFY_KEY)`) 560 upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`) 561 ext string 562 ) 563 flag.CommandLine.Parse(cmdline) 564 switch *atype { 565 case "zip": 566 ext = ".zip" 567 case "tar": 568 ext = ".tar.gz" 569 default: 570 log.Fatal("unknown archive type: ", atype) 571 } 572 573 var ( 574 env = build.Env() 575 basegeth = archiveBasename(*arch, version.Archive(env.Commit)) 576 geth = "geth-" + basegeth + ext 577 alltools = "geth-alltools-" + basegeth + ext 578 ) 579 maybeSkipArchive(env) 580 if err := build.WriteArchive(geth, gethArchiveFiles); err != nil { 581 log.Fatal(err) 582 } 583 if err := build.WriteArchive(alltools, allToolsArchiveFiles); err != nil { 584 log.Fatal(err) 585 } 586 for _, archive := range []string{geth, alltools} { 587 if err := archiveUpload(archive, *upload, *signer, *signify); err != nil { 588 log.Fatal(err) 589 } 590 } 591 } 592 593 func archiveBasename(arch string, archiveVersion string) string { 594 platform := runtime.GOOS + "-" + arch 595 if arch == "arm" { 596 platform += os.Getenv("GOARM") 597 } 598 if arch == "android" { 599 platform = "android-all" 600 } 601 if arch == "ios" { 602 platform = "ios-all" 603 } 604 return platform + "-" + archiveVersion 605 } 606 607 func archiveUpload(archive string, blobstore string, signer string, signifyVar string) error { 608 // If signing was requested, generate the signature files 609 if signer != "" { 610 key := getenvBase64(signer) 611 if err := build.PGPSignFile(archive, archive+".asc", string(key)); err != nil { 612 return err 613 } 614 } 615 if signifyVar != "" { 616 key := os.Getenv(signifyVar) 617 untrustedComment := "verify with geth-release.pub" 618 trustedComment := fmt.Sprintf("%s (%s)", archive, time.Now().UTC().Format(time.RFC1123)) 619 if err := signify.SignFile(archive, archive+".sig", key, untrustedComment, trustedComment); err != nil { 620 return err 621 } 622 } 623 // If uploading to Azure was requested, push the archive possibly with its signature 624 if blobstore != "" { 625 auth := build.AzureBlobstoreConfig{ 626 Account: strings.Split(blobstore, "/")[0], 627 Token: os.Getenv("AZURE_BLOBSTORE_TOKEN"), 628 Container: strings.SplitN(blobstore, "/", 2)[1], 629 } 630 if err := build.AzureBlobstoreUpload(archive, filepath.Base(archive), auth); err != nil { 631 return err 632 } 633 if signer != "" { 634 if err := build.AzureBlobstoreUpload(archive+".asc", filepath.Base(archive+".asc"), auth); err != nil { 635 return err 636 } 637 } 638 if signifyVar != "" { 639 if err := build.AzureBlobstoreUpload(archive+".sig", filepath.Base(archive+".sig"), auth); err != nil { 640 return err 641 } 642 } 643 } 644 return nil 645 } 646 647 // skips archiving for some build configurations. 648 func maybeSkipArchive(env build.Environment) { 649 if env.IsPullRequest { 650 log.Printf("skipping archive creation because this is a PR build") 651 os.Exit(0) 652 } 653 if env.Branch != "master" && !strings.HasPrefix(env.Tag, "v1.") { 654 log.Printf("skipping archive creation because branch %q, tag %q is not on the inclusion list", env.Branch, env.Tag) 655 os.Exit(0) 656 } 657 } 658 659 // Builds the docker images and optionally uploads them to Docker Hub. 660 func doDockerBuildx(cmdline []string) { 661 var ( 662 platform = flag.String("platform", "", `Push a multi-arch docker image for the specified architectures (usually "linux/amd64,linux/arm64")`) 663 hubImage = flag.String("hub", "ethereum/client-go", `Where to upload the docker image`) 664 upload = flag.Bool("upload", false, `Whether to trigger upload`) 665 ) 666 flag.CommandLine.Parse(cmdline) 667 668 // Skip building and pushing docker images for PR builds 669 env := build.Env() 670 maybeSkipArchive(env) 671 672 // Retrieve the upload credentials and authenticate 673 user := getenvBase64("DOCKER_HUB_USERNAME") 674 pass := getenvBase64("DOCKER_HUB_PASSWORD") 675 676 if len(user) > 0 && len(pass) > 0 { 677 auther := exec.Command("docker", "login", "-u", string(user), "--password-stdin") 678 auther.Stdin = bytes.NewReader(pass) 679 build.MustRun(auther) 680 } 681 // Retrieve the version infos to build and push to the following paths: 682 // - ethereum/client-go:latest - Pushes to the master branch, Geth only 683 // - ethereum/client-go:stable - Version tag publish on GitHub, Geth only 684 // - ethereum/client-go:alltools-latest - Pushes to the master branch, Geth & tools 685 // - ethereum/client-go:alltools-stable - Version tag publish on GitHub, Geth & tools 686 // - ethereum/client-go:release-<major>.<minor> - Version tag publish on GitHub, Geth only 687 // - ethereum/client-go:alltools-release-<major>.<minor> - Version tag publish on GitHub, Geth & tools 688 // - ethereum/client-go:v<major>.<minor>.<patch> - Version tag publish on GitHub, Geth only 689 // - ethereum/client-go:alltools-v<major>.<minor>.<patch> - Version tag publish on GitHub, Geth & tools 690 var tags []string 691 692 switch { 693 case env.Branch == "master": 694 tags = []string{"latest"} 695 case strings.HasPrefix(env.Tag, "v1."): 696 tags = []string{"stable", fmt.Sprintf("release-%v", version.Family), "v" + version.Semantic} 697 } 698 // Need to create a mult-arch builder 699 check := exec.Command("docker", "buildx", "inspect", "multi-arch-builder") 700 if check.Run() != nil { 701 build.MustRunCommand("docker", "buildx", "create", "--use", "--name", "multi-arch-builder", "--platform", *platform) 702 } 703 704 for _, spec := range []struct { 705 file string 706 base string 707 }{ 708 {file: "Dockerfile", base: fmt.Sprintf("%s:", *hubImage)}, 709 {file: "Dockerfile.alltools", base: fmt.Sprintf("%s:alltools-", *hubImage)}, 710 } { 711 for _, tag := range tags { // latest, stable etc 712 gethImage := fmt.Sprintf("%s%s", spec.base, tag) 713 cmd := exec.Command("docker", "buildx", "build", 714 "--build-arg", "COMMIT="+env.Commit, 715 "--build-arg", "VERSION="+version.WithMeta, 716 "--build-arg", "BUILDNUM="+env.Buildnum, 717 "--tag", gethImage, 718 "--platform", *platform, 719 "--file", spec.file, 720 ) 721 if *upload { 722 cmd.Args = append(cmd.Args, "--push") 723 } 724 cmd.Args = append(cmd.Args, ".") 725 build.MustRun(cmd) 726 } 727 } 728 } 729 730 // Debian Packaging 731 func doDebianSource(cmdline []string) { 732 var ( 733 cachedir = flag.String("cachedir", "./build/cache", `Filesystem path to cache the downloaded Go bundles at`) 734 signer = flag.String("signer", "", `Signing key name, also used as package author`) 735 upload = flag.String("upload", "", `Where to upload the source package (usually "ethereum/ethereum")`) 736 sshUser = flag.String("sftp-user", "", `Username for SFTP upload (usually "geth-ci")`) 737 workdir = flag.String("workdir", "", `Output directory for packages (uses temp dir if unset)`) 738 now = time.Now() 739 ) 740 flag.CommandLine.Parse(cmdline) 741 *workdir = makeWorkdir(*workdir) 742 env := build.Env() 743 tc := new(build.GoToolchain) 744 maybeSkipArchive(env) 745 746 // Import the signing key. 747 if key := getenvBase64("PPA_SIGNING_KEY"); len(key) > 0 { 748 gpg := exec.Command("gpg", "--import") 749 gpg.Stdin = bytes.NewReader(key) 750 build.MustRun(gpg) 751 } 752 // Download and verify the Go source packages. 753 var ( 754 gobootbundles = downloadGoBootstrapSources(*cachedir) 755 gobundle = downloadGoSources(*cachedir) 756 ) 757 // Download all the dependencies needed to build the sources and run the ci script 758 srcdepfetch := tc.Go("mod", "download") 759 srcdepfetch.Env = append(srcdepfetch.Env, "GOPATH="+filepath.Join(*workdir, "modgopath")) 760 build.MustRun(srcdepfetch) 761 762 cidepfetch := tc.Go("run", "./build/ci.go") 763 cidepfetch.Env = append(cidepfetch.Env, "GOPATH="+filepath.Join(*workdir, "modgopath")) 764 cidepfetch.Run() // Command fails, don't care, we only need the deps to start it 765 766 // Create Debian packages and upload them. 767 for _, pkg := range debPackages { 768 for _, distro := range debDistros { 769 // Prepare the debian package with the go-ethereum sources. 770 meta := newDebMetadata(distro, *signer, env, now, pkg.Name, pkg.Version, pkg.Executables) 771 pkgdir := stageDebianSource(*workdir, meta) 772 773 // Add bootstrapper Go source code 774 for i, gobootbundle := range gobootbundles { 775 if err := build.ExtractArchive(gobootbundle, pkgdir); err != nil { 776 log.Fatalf("Failed to extract bootstrapper Go sources: %v", err) 777 } 778 if err := os.Rename(filepath.Join(pkgdir, "go"), filepath.Join(pkgdir, fmt.Sprintf(".goboot-%d", i+1))); err != nil { 779 log.Fatalf("Failed to rename bootstrapper Go source folder: %v", err) 780 } 781 } 782 // Add builder Go source code 783 if err := build.ExtractArchive(gobundle, pkgdir); err != nil { 784 log.Fatalf("Failed to extract builder Go sources: %v", err) 785 } 786 if err := os.Rename(filepath.Join(pkgdir, "go"), filepath.Join(pkgdir, ".go")); err != nil { 787 log.Fatalf("Failed to rename builder Go source folder: %v", err) 788 } 789 // Add all dependency modules in compressed form 790 os.MkdirAll(filepath.Join(pkgdir, ".mod", "cache"), 0755) 791 if err := cp.CopyAll(filepath.Join(pkgdir, ".mod", "cache", "download"), filepath.Join(*workdir, "modgopath", "pkg", "mod", "cache", "download")); err != nil { 792 log.Fatalf("Failed to copy Go module dependencies: %v", err) 793 } 794 // Run the packaging and upload to the PPA 795 debuild := exec.Command("debuild", "-S", "-sa", "-us", "-uc", "-d", "-Zxz", "-nc") 796 debuild.Dir = pkgdir 797 build.MustRun(debuild) 798 799 var ( 800 basename = fmt.Sprintf("%s_%s", meta.Name(), meta.VersionString()) 801 source = filepath.Join(*workdir, basename+".tar.xz") 802 dsc = filepath.Join(*workdir, basename+".dsc") 803 changes = filepath.Join(*workdir, basename+"_source.changes") 804 buildinfo = filepath.Join(*workdir, basename+"_source.buildinfo") 805 ) 806 if *signer != "" { 807 build.MustRunCommand("debsign", changes) 808 } 809 if *upload != "" { 810 ppaUpload(*workdir, *upload, *sshUser, []string{source, dsc, changes, buildinfo}) 811 } 812 } 813 } 814 } 815 816 // downloadGoBootstrapSources downloads the Go source tarball(s) that will be used 817 // to bootstrap the builder Go. 818 func downloadGoBootstrapSources(cachedir string) []string { 819 csdb := download.MustLoadChecksums("build/checksums.txt") 820 821 var bundles []string 822 for _, booter := range []string{"ppa-builder-1.19", "ppa-builder-1.21", "ppa-builder-1.23"} { 823 gobootVersion, err := csdb.FindVersion(booter) 824 if err != nil { 825 log.Fatal(err) 826 } 827 file := fmt.Sprintf("go%s.src.tar.gz", gobootVersion) 828 dst := filepath.Join(cachedir, file) 829 if err := csdb.DownloadFileFromKnownURL(dst); err != nil { 830 log.Fatal(err) 831 } 832 bundles = append(bundles, dst) 833 } 834 return bundles 835 } 836 837 // downloadGoSources downloads the Go source tarball. 838 func downloadGoSources(cachedir string) string { 839 csdb := download.MustLoadChecksums("build/checksums.txt") 840 dlgoVersion, err := csdb.FindVersion("golang") 841 if err != nil { 842 log.Fatal(err) 843 } 844 file := fmt.Sprintf("go%s.src.tar.gz", dlgoVersion) 845 dst := filepath.Join(cachedir, file) 846 if err := csdb.DownloadFileFromKnownURL(dst); err != nil { 847 log.Fatal(err) 848 } 849 return dst 850 } 851 852 func ppaUpload(workdir, ppa, sshUser string, files []string) { 853 p := strings.Split(ppa, "/") 854 if len(p) != 2 { 855 log.Fatal("-upload PPA name must contain single /") 856 } 857 if sshUser == "" { 858 sshUser = p[0] 859 } 860 incomingDir := fmt.Sprintf("~%s/ubuntu/%s", p[0], p[1]) 861 // Create the SSH identity file if it doesn't exist. 862 var idfile string 863 if sshkey := getenvBase64("PPA_SSH_KEY"); len(sshkey) > 0 { 864 idfile = filepath.Join(workdir, "sshkey") 865 if !build.FileExist(idfile) { 866 os.WriteFile(idfile, sshkey, 0600) 867 } 868 } 869 // Upload. This doesn't always work, so try up to three times. 870 dest := sshUser + "@ppa.launchpad.net" 871 for i := 0; i < 3; i++ { 872 err := build.UploadSFTP(idfile, dest, incomingDir, files) 873 if err == nil { 874 return 875 } 876 log.Println("PPA upload failed:", err) 877 time.Sleep(5 * time.Second) 878 } 879 log.Fatal("PPA upload failed all attempts.") 880 } 881 882 func getenvBase64(variable string) []byte { 883 dec, err := base64.StdEncoding.DecodeString(os.Getenv(variable)) 884 if err != nil { 885 log.Fatal("invalid base64 " + variable) 886 } 887 return []byte(dec) 888 } 889 890 func makeWorkdir(wdflag string) string { 891 var err error 892 if wdflag != "" { 893 err = os.MkdirAll(wdflag, 0744) 894 } else { 895 wdflag, err = os.MkdirTemp("", "geth-build-") 896 } 897 if err != nil { 898 log.Fatal(err) 899 } 900 return wdflag 901 } 902 903 func isUnstableBuild(env build.Environment) bool { 904 if env.Tag != "" { 905 return false 906 } 907 return true 908 } 909 910 type debPackage struct { 911 Name string // the name of the Debian package to produce, e.g. "ethereum" 912 Version string // the clean version of the debPackage, e.g. 1.8.12, without any metadata 913 Executables []debExecutable // executables to be included in the package 914 } 915 916 type debMetadata struct { 917 Env build.Environment 918 PackageName string 919 920 // go-ethereum version being built. Note that this 921 // is not the debian package version. The package version 922 // is constructed by VersionString. 923 Version string 924 925 Author string // "name <email>", also selects signing key 926 Distro, Time string 927 Executables []debExecutable 928 } 929 930 type debExecutable struct { 931 PackageName string 932 BinaryName string 933 Description string 934 } 935 936 // Package returns the name of the package if present, or 937 // fallbacks to BinaryName 938 func (d debExecutable) Package() string { 939 if d.PackageName != "" { 940 return d.PackageName 941 } 942 return d.BinaryName 943 } 944 945 func newDebMetadata(distro, author string, env build.Environment, t time.Time, name string, version string, exes []debExecutable) debMetadata { 946 if author == "" { 947 // No signing key, use default author. 948 author = "Ethereum Builds <fjl@ethereum.org>" 949 } 950 return debMetadata{ 951 PackageName: name, 952 Env: env, 953 Author: author, 954 Distro: distro, 955 Version: version, 956 Time: t.Format(time.RFC1123Z), 957 Executables: exes, 958 } 959 } 960 961 // Name returns the name of the metapackage that depends 962 // on all executable packages. 963 func (meta debMetadata) Name() string { 964 if isUnstableBuild(meta.Env) { 965 return meta.PackageName + "-unstable" 966 } 967 return meta.PackageName 968 } 969 970 // VersionString returns the debian version of the packages. 971 func (meta debMetadata) VersionString() string { 972 vsn := meta.Version 973 if meta.Env.Buildnum != "" { 974 vsn += "+build" + meta.Env.Buildnum 975 } 976 if meta.Distro != "" { 977 vsn += "+" + meta.Distro 978 } 979 return vsn 980 } 981 982 // ExeList returns the list of all executable packages. 983 func (meta debMetadata) ExeList() string { 984 names := make([]string, len(meta.Executables)) 985 for i, e := range meta.Executables { 986 names[i] = meta.ExeName(e) 987 } 988 return strings.Join(names, ", ") 989 } 990 991 // ExeName returns the package name of an executable package. 992 func (meta debMetadata) ExeName(exe debExecutable) string { 993 if isUnstableBuild(meta.Env) { 994 return exe.Package() + "-unstable" 995 } 996 return exe.Package() 997 } 998 999 // ExeConflicts returns the content of the Conflicts field 1000 // for executable packages. 1001 func (meta debMetadata) ExeConflicts(exe debExecutable) string { 1002 if isUnstableBuild(meta.Env) { 1003 // Set up the conflicts list so that the *-unstable packages 1004 // cannot be installed alongside the regular version. 1005 // 1006 // https://www.debian.org/doc/debian-policy/ch-relationships.html 1007 // is very explicit about Conflicts: and says that Breaks: should 1008 // be preferred and the conflicting files should be handled via 1009 // alternates. We might do this eventually but using a conflict is 1010 // easier now. 1011 return "ethereum, " + exe.Package() 1012 } 1013 return "" 1014 } 1015 1016 func stageDebianSource(tmpdir string, meta debMetadata) (pkgdir string) { 1017 pkg := meta.Name() + "-" + meta.VersionString() 1018 pkgdir = filepath.Join(tmpdir, pkg) 1019 if err := os.Mkdir(pkgdir, 0755); err != nil { 1020 log.Fatal(err) 1021 } 1022 // Copy the source code. 1023 build.MustRunCommand("git", "checkout-index", "-a", "--prefix", pkgdir+string(filepath.Separator)) 1024 1025 // Put the debian build files in place. 1026 debian := filepath.Join(pkgdir, "debian") 1027 build.Render("build/deb/"+meta.PackageName+"/deb.rules", filepath.Join(debian, "rules"), 0755, meta) 1028 build.Render("build/deb/"+meta.PackageName+"/deb.changelog", filepath.Join(debian, "changelog"), 0644, meta) 1029 build.Render("build/deb/"+meta.PackageName+"/deb.control", filepath.Join(debian, "control"), 0644, meta) 1030 build.Render("build/deb/"+meta.PackageName+"/deb.copyright", filepath.Join(debian, "copyright"), 0644, meta) 1031 build.RenderString("8\n", filepath.Join(debian, "compat"), 0644, meta) 1032 build.RenderString("3.0 (native)\n", filepath.Join(debian, "source/format"), 0644, meta) 1033 for _, exe := range meta.Executables { 1034 install := filepath.Join(debian, meta.ExeName(exe)+".install") 1035 docs := filepath.Join(debian, meta.ExeName(exe)+".docs") 1036 build.Render("build/deb/"+meta.PackageName+"/deb.install", install, 0644, exe) 1037 build.Render("build/deb/"+meta.PackageName+"/deb.docs", docs, 0644, exe) 1038 } 1039 return pkgdir 1040 } 1041 1042 // Windows installer 1043 func doWindowsInstaller(cmdline []string) { 1044 // Parse the flags and make skip installer generation on PRs 1045 var ( 1046 arch = flag.String("arch", runtime.GOARCH, "Architecture for cross build packaging") 1047 signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. WINDOWS_SIGNING_KEY)`) 1048 signify = flag.String("signify key", "", `Environment variable holding the signify signing key (e.g. WINDOWS_SIGNIFY_KEY)`) 1049 upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`) 1050 workdir = flag.String("workdir", "", `Output directory for packages (uses temp dir if unset)`) 1051 ) 1052 flag.CommandLine.Parse(cmdline) 1053 *workdir = makeWorkdir(*workdir) 1054 env := build.Env() 1055 maybeSkipArchive(env) 1056 1057 // Aggregate binaries that are included in the installer 1058 var ( 1059 devTools []string 1060 allTools []string 1061 gethTool string 1062 ) 1063 for _, file := range allToolsArchiveFiles { 1064 if file == "COPYING" { // license, copied later 1065 continue 1066 } 1067 allTools = append(allTools, filepath.Base(file)) 1068 if filepath.Base(file) == "geth.exe" { 1069 gethTool = file 1070 } else { 1071 devTools = append(devTools, file) 1072 } 1073 } 1074 1075 // Render NSIS scripts: Installer NSIS contains two installer sections, 1076 // first section contains the geth binary, second section holds the dev tools. 1077 templateData := map[string]interface{}{ 1078 "License": "COPYING", 1079 "Geth": gethTool, 1080 "DevTools": devTools, 1081 } 1082 build.Render("build/nsis.geth.nsi", filepath.Join(*workdir, "geth.nsi"), 0644, nil) 1083 build.Render("build/nsis.install.nsh", filepath.Join(*workdir, "install.nsh"), 0644, templateData) 1084 build.Render("build/nsis.uninstall.nsh", filepath.Join(*workdir, "uninstall.nsh"), 0644, allTools) 1085 build.Render("build/nsis.pathupdate.nsh", filepath.Join(*workdir, "PathUpdate.nsh"), 0644, nil) 1086 build.Render("build/nsis.envvarupdate.nsh", filepath.Join(*workdir, "EnvVarUpdate.nsh"), 0644, nil) 1087 if err := cp.CopyFile(filepath.Join(*workdir, "SimpleFC.dll"), "build/nsis.simplefc.dll"); err != nil { 1088 log.Fatalf("Failed to copy SimpleFC.dll: %v", err) 1089 } 1090 if err := cp.CopyFile(filepath.Join(*workdir, "COPYING"), "COPYING"); err != nil { 1091 log.Fatalf("Failed to copy copyright note: %v", err) 1092 } 1093 // Build the installer. This assumes that all the needed files have been previously 1094 // built (don't mix building and packaging to keep cross compilation complexity to a 1095 // minimum). 1096 ver := strings.Split(version.Semantic, ".") 1097 if env.Commit != "" { 1098 ver[2] += "-" + env.Commit[:8] 1099 } 1100 installer, err := filepath.Abs("geth-" + archiveBasename(*arch, version.Archive(env.Commit)) + ".exe") 1101 if err != nil { 1102 log.Fatalf("Failed to convert installer file path: %v", err) 1103 } 1104 build.MustRunCommand("makensis.exe", 1105 "/DOUTPUTFILE="+installer, 1106 "/DMAJORVERSION="+ver[0], 1107 "/DMINORVERSION="+ver[1], 1108 "/DBUILDVERSION="+ver[2], 1109 "/DARCH="+*arch, 1110 filepath.Join(*workdir, "geth.nsi"), 1111 ) 1112 // Sign and publish installer. 1113 if err := archiveUpload(installer, *upload, *signer, *signify); err != nil { 1114 log.Fatal(err) 1115 } 1116 } 1117 1118 // Binary distribution cleanups 1119 1120 func doPurge(cmdline []string) { 1121 var ( 1122 store = flag.String("store", "", `Destination from where to purge archives (usually "gethstore/builds")`) 1123 limit = flag.Int("days", 30, `Age threshold above which to delete unstable archives`) 1124 ) 1125 flag.CommandLine.Parse(cmdline) 1126 1127 if env := build.Env(); !env.IsCronJob { 1128 log.Printf("skipping because not a cron job") 1129 os.Exit(0) 1130 } 1131 // Create the azure authentication and list the current archives 1132 auth := build.AzureBlobstoreConfig{ 1133 Account: strings.Split(*store, "/")[0], 1134 Token: os.Getenv("AZURE_BLOBSTORE_TOKEN"), 1135 Container: strings.SplitN(*store, "/", 2)[1], 1136 } 1137 blobs, err := build.AzureBlobstoreList(auth) 1138 if err != nil { 1139 log.Fatal(err) 1140 } 1141 fmt.Printf("Found %d blobs\n", len(blobs)) 1142 1143 // Iterate over the blobs, collect and sort all unstable builds 1144 for i := 0; i < len(blobs); i++ { 1145 if !strings.Contains(*blobs[i].Name, "unstable") { 1146 blobs = append(blobs[:i], blobs[i+1:]...) 1147 i-- 1148 } 1149 } 1150 for i := 0; i < len(blobs); i++ { 1151 for j := i + 1; j < len(blobs); j++ { 1152 if blobs[i].Properties.LastModified.After(*blobs[j].Properties.LastModified) { 1153 blobs[i], blobs[j] = blobs[j], blobs[i] 1154 } 1155 } 1156 } 1157 // Filter out all archives more recent that the given threshold 1158 for i, blob := range blobs { 1159 if time.Since(*blob.Properties.LastModified) < time.Duration(*limit)*24*time.Hour { 1160 blobs = blobs[:i] 1161 break 1162 } 1163 } 1164 fmt.Printf("Deleting %d blobs\n", len(blobs)) 1165 // Delete all marked as such and return 1166 if err := build.AzureBlobstoreDelete(auth, blobs); err != nil { 1167 log.Fatal(err) 1168 } 1169 } 1170 1171 func doSanityCheck() { 1172 csdb := download.MustLoadChecksums("build/checksums.txt") 1173 csdb.DownloadAndVerifyAll() 1174 }