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  }