github.com/klaytn/klaytn@v1.12.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  package main
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"encoding/base64"
    26  	"flag"
    27  	"fmt"
    28  	"go/parser"
    29  	"go/token"
    30  	"log"
    31  	"os"
    32  	"os/exec"
    33  	"path/filepath"
    34  	"regexp"
    35  	"runtime"
    36  	"strconv"
    37  	"strings"
    38  	"time"
    39  
    40  	"github.com/klaytn/klaytn/utils/build"
    41  )
    42  
    43  var (
    44  
    45  	// Files that end up in the klay*.zip archive.
    46  	klayArchiveFiles = []string{
    47  		"COPYING",
    48  		executablePath("klay"),
    49  	}
    50  
    51  	// Files that end up in the klay-alltools*.zip archive.
    52  	allToolsArchiveFiles = []string{
    53  		"COPYING",
    54  		executablePath("klay"),
    55  	}
    56  
    57  	// A debian package is created for all executables listed here.
    58  	debExecutables = []debExecutable{
    59  		{
    60  			Name:        "klay",
    61  			Description: "Klaytn CLI client.",
    62  		},
    63  	}
    64  
    65  	// Distros for which packages are created.
    66  	// Note: vivid is unsupported because there is no golang-1.6 package for it.
    67  	// Note: wily is unsupported because it was officially deprecated on lanchpad.
    68  	// Note: yakkety is unsupported because it was officially deprecated on lanchpad.
    69  	// Note: zesty is unsupported because it was officially deprecated on lanchpad.
    70  	debDistros = []string{"trusty", "xenial", "artful", "bionic"}
    71  )
    72  
    73  var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin"))
    74  
    75  func executablePath(name string) string {
    76  	if runtime.GOOS == "windows" {
    77  		name += ".exe"
    78  	}
    79  	return filepath.Join(GOBIN, name)
    80  }
    81  
    82  func main() {
    83  	log.SetFlags(log.Lshortfile)
    84  
    85  	if _, err := os.Stat(filepath.Join("build", "ci.go")); os.IsNotExist(err) {
    86  		log.Fatal("this script must be run from the root of the repository")
    87  	}
    88  	if len(os.Args) < 2 {
    89  		log.Fatal("need subcommand as first argument")
    90  	}
    91  	switch os.Args[1] {
    92  	case "install":
    93  		doInstall(os.Args[2:])
    94  	case "test":
    95  		doTest(os.Args[2:])
    96  	case "cover":
    97  		doCover(os.Args[2:])
    98  	case "lint":
    99  		doLint(os.Args[2:], true)
   100  	case "lint-try":
   101  		doLint(os.Args[2:], false)
   102  	case "archive":
   103  		doArchive(os.Args[2:])
   104  	case "debsrc":
   105  		doDebianSource(os.Args[2:])
   106  	case "nsis":
   107  		doWindowsInstaller(os.Args[2:])
   108  	case "aar":
   109  		doAndroidArchive(os.Args[2:])
   110  	case "xcode":
   111  		doXCodeFramework(os.Args[2:])
   112  	default:
   113  		log.Fatal("unknown command ", os.Args[1])
   114  	}
   115  }
   116  
   117  // Compiling
   118  
   119  func doInstall(cmdline []string) {
   120  	var (
   121  		arch = flag.String("arch", "", "Architecture to cross build for")
   122  		cc   = flag.String("cc", "", "C compiler to cross build with")
   123  		tags = flag.String("tags", "", "Build tags")
   124  	)
   125  	flag.CommandLine.Parse(cmdline)
   126  	env := build.Env()
   127  
   128  	var tagsArgs []string
   129  	if len(*tags) > 0 {
   130  		tagsArgs = append(tagsArgs, "-tags", *tags)
   131  	}
   132  
   133  	// Check Go version. People regularly open issues about compilation
   134  	// failure with outdated Go. This should save them the trouble.
   135  	if !strings.Contains(runtime.Version(), "devel") {
   136  		// Figure out the minor version number since we can't textually compare (1.10 < 1.9)
   137  		var minor int
   138  		fmt.Sscanf(strings.TrimPrefix(runtime.Version(), "go1."), "%d", &minor)
   139  
   140  		if minor < 9 {
   141  			log.Println("You have Go version", runtime.Version())
   142  			log.Println("klaytn requires at least Go version 1.9 and cannot")
   143  			log.Println("be compiled with an earlier version. Please upgrade your Go installation.")
   144  			os.Exit(1)
   145  		}
   146  	}
   147  	// Compile packages given as arguments, or everything if there are no arguments.
   148  	packages := []string{"./..."}
   149  	if flag.NArg() > 0 {
   150  		packages = flag.Args()
   151  	}
   152  
   153  	if *arch == "" || *arch == runtime.GOARCH {
   154  		goinstall := goTool("install", buildFlags(env)...)
   155  		goinstall.Args = append(goinstall.Args, "-v")
   156  		goinstall.Args = append(goinstall.Args, tagsArgs...)
   157  		// goinstall.Args = append(goinstall.Args, "-race")
   158  		goinstall.Args = append(goinstall.Args, packages...)
   159  		build.MustRun(goinstall)
   160  		return
   161  	}
   162  	// If we are cross compiling to ARMv5 ARMv6 or ARMv7, clean any previous builds
   163  	if *arch == "arm" {
   164  		os.RemoveAll(filepath.Join(runtime.GOROOT(), "pkg", runtime.GOOS+"_arm"))
   165  		for _, path := range filepath.SplitList(build.GOPATH()) {
   166  			os.RemoveAll(filepath.Join(path, "pkg", runtime.GOOS+"_arm"))
   167  		}
   168  	}
   169  	// Seems we are cross compiling, work around forbidden GOBIN
   170  	goinstall := goToolArch(*arch, *cc, "install", buildFlags(env)...)
   171  	goinstall.Args = append(goinstall.Args, "-v")
   172  	goinstall.Args = append(goinstall.Args, tagsArgs...)
   173  	goinstall.Args = append(goinstall.Args, []string{"-buildmode", "archive"}...)
   174  	goinstall.Args = append(goinstall.Args, packages...)
   175  	build.MustRun(goinstall)
   176  
   177  	if cmds, err := os.ReadDir("cmd"); err == nil {
   178  		for _, cmd := range cmds {
   179  			pkgs, err := parser.ParseDir(token.NewFileSet(), filepath.Join(".", "cmd", cmd.Name()), nil, parser.PackageClauseOnly)
   180  			if err != nil {
   181  				log.Fatal(err)
   182  			}
   183  			for name := range pkgs {
   184  				if name == "main" {
   185  					gobuild := goToolArch(*arch, *cc, "build", buildFlags(env)...)
   186  					gobuild.Args = append(gobuild.Args, "-v")
   187  					gobuild.Args = append(goinstall.Args, tagsArgs...)
   188  					gobuild.Args = append(gobuild.Args, []string{"-o", executablePath(cmd.Name())}...)
   189  					gobuild.Args = append(gobuild.Args, "."+string(filepath.Separator)+filepath.Join("cmd", cmd.Name()))
   190  					build.MustRun(gobuild)
   191  					break
   192  				}
   193  			}
   194  		}
   195  	}
   196  }
   197  
   198  func buildFlags(env build.Environment) (flags []string) {
   199  	var ld []string
   200  	if env.Commit != "" {
   201  		ld = append(ld, "-X", "main.gitCommit="+env.Commit)
   202  		ld = append(ld, "-X", "github.com/klaytn/klaytn/cmd/utils/nodecmd.gitCommit="+env.Commit)
   203  	}
   204  	if env.Tag != "" {
   205  		ld = append(ld, "-X", "github.com/klaytn/klaytn/cmd/utils/nodecmd.gitTag="+env.Tag)
   206  	}
   207  	if runtime.GOOS == "darwin" {
   208  		ld = append(ld, "-s")
   209  	}
   210  
   211  	if env.IsDisabledSymbolTable {
   212  		ld = append(ld, "-s")
   213  	}
   214  	if env.IsStaticLink {
   215  		// Pass the static link flag to the external linker.
   216  		// By default, cmd/link will use external linking mode when non-standard cgo packages are involved.
   217  		ld = append(ld, "-linkmode", "external", "-extldflags", "-static")
   218  	}
   219  	if env.IsKlaytnRaceDetectionOn {
   220  		flags = append(flags, "-race")
   221  	}
   222  	if len(ld) > 0 {
   223  		flags = append(flags, "-ldflags", strings.Join(ld, " "))
   224  	}
   225  	return flags
   226  }
   227  
   228  func goTool(subcmd string, args ...string) *exec.Cmd {
   229  	return goToolArch(runtime.GOARCH, os.Getenv("CC"), subcmd, args...)
   230  }
   231  
   232  func goToolArch(arch string, cc string, subcmd string, args ...string) *exec.Cmd {
   233  	cmd := build.GoTool(subcmd, args...)
   234  	cmd.Env = []string{"GOPATH=" + build.GOPATH()}
   235  	if arch == "" || arch == runtime.GOARCH {
   236  		cmd.Env = append(cmd.Env, "GOBIN="+GOBIN)
   237  	} else {
   238  		cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
   239  		cmd.Env = append(cmd.Env, "GOARCH="+arch)
   240  	}
   241  	if cc != "" {
   242  		cmd.Env = append(cmd.Env, "CC="+cc)
   243  	}
   244  	// CKZG by default is not portable, append the necessary build flags to make
   245  	// it not rely on modern CPU instructions and enable linking against.
   246  	cmd.Env = append(cmd.Env, "CGO_CFLAGS=-O2 -g -D__BLST_PORTABLE__")
   247  	for _, e := range os.Environ() {
   248  		if strings.HasPrefix(e, "GOPATH=") || strings.HasPrefix(e, "GOBIN=") {
   249  			continue
   250  		}
   251  		cmd.Env = append(cmd.Env, e)
   252  	}
   253  	return cmd
   254  }
   255  
   256  // Running The Tests
   257  //
   258  // "tests" also includes static analysis tools such as vet.
   259  
   260  func doTest(cmdline []string) {
   261  	var (
   262  		parallel = flag.Int("p", 0, "The number of parallel test executions (default: the number of CPUs available)")
   263  		excludes = flag.String("exclude", "", "Comma-separated top-level directories to be excluded in test")
   264  	)
   265  	flag.CommandLine.Parse(cmdline)
   266  	env := build.Env()
   267  
   268  	packages := []string{"./..."}
   269  	if len(flag.CommandLine.Args()) > 0 {
   270  		packages = flag.CommandLine.Args()
   271  	}
   272  
   273  	if *excludes != "" {
   274  		packages = build.ExcludePackages(packages, strings.Split(*excludes, ","))
   275  	}
   276  
   277  	// Run analysis tools before the tests.
   278  	build.MustRun(goTool("vet", packages...))
   279  
   280  	// Run the actual tests.
   281  	gotest := goTool("test", buildFlags(env)...)
   282  	if *parallel != 0 {
   283  		gotest.Args = append(gotest.Args, "-p", strconv.Itoa(*parallel))
   284  	}
   285  	gotest.Args = append(gotest.Args, "--timeout=30m")
   286  	gotest.Args = append(gotest.Args, packages...)
   287  	build.MustRun(gotest)
   288  }
   289  
   290  func doCover(cmdline []string) {
   291  	var (
   292  		parallel   = flag.Int("p", 0, "The number of parallel coverage test executions (default: the number of CPUs available)")
   293  		excludes   = flag.String("exclude", "", "Comma-separated top-level directories to be excluded in coverage test")
   294  		outputFile = flag.String("coverprofile", "coverage.out", "The coverage profile file will be generated by coverage test")
   295  	)
   296  	flag.CommandLine.Parse(cmdline)
   297  	env := build.Env()
   298  
   299  	packages := []string{"./..."}
   300  	if len(flag.CommandLine.Args()) > 0 {
   301  		packages = flag.CommandLine.Args()
   302  	}
   303  
   304  	if *excludes != "" {
   305  		packages = build.ExcludePackages(packages, strings.Split(*excludes, ","))
   306  	}
   307  
   308  	coverPackages := []string{"./..."}
   309  	coverExcludes := []string{
   310  		"/tests",
   311  		"/metric",
   312  		"/build",
   313  		"/client",
   314  		"/contracts",
   315  		"/simulations",
   316  		"/api",
   317  		"/fork",
   318  		"/mocks",
   319  	}
   320  
   321  	coverPackages = build.ExcludePackages(coverPackages, coverExcludes)
   322  	coverPackagesString := strings.Join(coverPackages, ",")
   323  
   324  	// Run analysis tools before the tests.
   325  	build.MustRun(goTool("vet", packages...))
   326  
   327  	// Generate a coverage output file.
   328  	build.MustRunCommand("sh", "-c", "echo 'mode: atomic' > "+*outputFile)
   329  
   330  	// Run the actual tests.
   331  	gotest := goTool("test", buildFlags(env)...)
   332  	if *parallel != 0 {
   333  		gotest.Args = append(gotest.Args, "-p", strconv.Itoa(*parallel))
   334  	}
   335  
   336  	gotest.Args = append(gotest.Args, "-cover", "-covermode=atomic", "-coverprofile="+*outputFile)
   337  	gotest.Args = append(gotest.Args, "-coverpkg", coverPackagesString)
   338  	gotest.Args = append(gotest.Args, "--timeout=30m")
   339  	gotest.Args = append(gotest.Args, packages...)
   340  	build.MustRun(gotest)
   341  }
   342  
   343  // runs gometalinter on requested packages and exits immediately when linter warning observed if exitOnError is true
   344  // if exitOnError is false, prepare a report file for linters and run additional linters without stopping
   345  func doLint(cmdline []string, exitOnError bool) {
   346  	var (
   347  		vFlag      = flag.Bool("v", false, "verbose output")
   348  		newFromRev = flag.String("new-from-rev", "", "Show only new issues created after git revision REV")
   349  
   350  		fname = "linter_report.txt"
   351  	)
   352  	flag.CommandLine.Parse(cmdline)
   353  
   354  	packages := []string{"./..."}
   355  	if len(flag.CommandLine.Args()) > 0 {
   356  		packages = flag.CommandLine.Args()
   357  	}
   358  
   359  	lintBin := installLinter()
   360  
   361  	// linters for "lint" command
   362  	lintersSet := [][]string{
   363  		{
   364  			"--presets=format",
   365  			"--presets=performance",
   366  		},
   367  	}
   368  
   369  	if !exitOnError {
   370  		// Prepare a report file for linters
   371  		fileOut, err := os.Create(fname)
   372  		defer fileOut.Close()
   373  		if err != nil {
   374  			log.Fatal(err)
   375  		}
   376  		fmt.Printf("Generating a linter report %s using above linters.\n", fname)
   377  
   378  		oldStdout := os.Stdout
   379  		os.Stdout = fileOut
   380  		defer func() {
   381  			// Restore stdout
   382  			os.Stdout = oldStdout
   383  			fmt.Printf("Successfully generating %s.\n", fname)
   384  		}()
   385  
   386  		// linters for "lint-try" command
   387  		lintersSet = [][]string{
   388  			{
   389  				"--enable=misspell",
   390  				"--enable=goconst",
   391  			},
   392  			{
   393  				"--enable=dupl",
   394  				"--enable=errcheck",
   395  				"--enable=ineffassign",
   396  				"--enable=unparam",
   397  				"--enable=unused",
   398  			},
   399  			{"--enable=unconvert"},
   400  			{"--enable=gosimple"},
   401  			{"--enable=staticcheck"},
   402  			{"--enable=gocyclo"},
   403  		}
   404  	}
   405  
   406  	for _, linters := range lintersSet {
   407  		configs := []string{
   408  			"run",
   409  			"--tests",
   410  			"--disable-all",
   411  			"--timeout=10m",
   412  		}
   413  		if *vFlag {
   414  			configs = append(configs, "-v")
   415  		}
   416  		if *newFromRev != "" {
   417  			configs = append(configs, "--new-from-rev="+*newFromRev)
   418  		}
   419  		configs = append(configs, linters...)
   420  		args := append(configs, packages...)
   421  		if exitOnError {
   422  			build.MustRunCommand(lintBin, args...)
   423  		} else {
   424  			build.TryRunCommand(lintBin, args...)
   425  		}
   426  	}
   427  }
   428  
   429  // Release Packaging
   430  func doArchive(cmdline []string) {
   431  	var (
   432  		arch   = flag.String("arch", runtime.GOARCH, "Architecture cross packaging")
   433  		atype  = flag.String("type", "zip", "Type of archive to write (zip|tar)")
   434  		signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`)
   435  		upload = flag.String("upload", "", `Destination to upload the archives`)
   436  		ext    string
   437  	)
   438  	flag.CommandLine.Parse(cmdline)
   439  	switch *atype {
   440  	case "zip":
   441  		ext = ".zip"
   442  	case "tar":
   443  		ext = ".tar.gz"
   444  	default:
   445  		log.Fatal("unknown archive type: ", atype)
   446  	}
   447  
   448  	var (
   449  		env      = build.Env()
   450  		base     = archiveBasename(*arch, env)
   451  		klaybin  = "klay-" + base + ext
   452  		alltools = "klay-alltools-" + base + ext
   453  	)
   454  	maybeSkipArchive(env)
   455  	if err := build.WriteArchive(klaybin, klayArchiveFiles); err != nil {
   456  		log.Fatal(err)
   457  	}
   458  	if err := build.WriteArchive(alltools, allToolsArchiveFiles); err != nil {
   459  		log.Fatal(err)
   460  	}
   461  	for _, archive := range []string{klaybin, alltools} {
   462  		if err := archiveUpload(archive, *upload, *signer); err != nil {
   463  			log.Fatal(err)
   464  		}
   465  	}
   466  }
   467  
   468  func archiveBasename(arch string, env build.Environment) string {
   469  	platform := runtime.GOOS + "-" + arch
   470  	if arch == "arm" {
   471  		platform += os.Getenv("GOARM")
   472  	}
   473  	if arch == "android" {
   474  		platform = "android-all"
   475  	}
   476  	if arch == "ios" {
   477  		platform = "ios-all"
   478  	}
   479  	return platform + "-" + archiveVersion(env)
   480  }
   481  
   482  func archiveVersion(env build.Environment) string {
   483  	version := build.VERSION()
   484  	if isUnstableBuild(env) {
   485  		version += "-unstable"
   486  	}
   487  	if env.Commit != "" {
   488  		version += "-" + env.Commit[:8]
   489  	}
   490  	return version
   491  }
   492  
   493  func archiveUpload(archive string, blobstore string, signer string) error {
   494  	// If signing was requested, generate the signature files
   495  	if signer != "" {
   496  		pgpkey, err := base64.StdEncoding.DecodeString(os.Getenv(signer))
   497  		if err != nil {
   498  			return fmt.Errorf("invalid base64 %s", signer)
   499  		}
   500  		if err := build.PGPSignFile(archive, archive+".asc", string(pgpkey)); err != nil {
   501  			return err
   502  		}
   503  	}
   504  	return nil
   505  }
   506  
   507  // skips archiving for some build configurations.
   508  func maybeSkipArchive(env build.Environment) {
   509  	if env.IsPullRequest {
   510  		log.Printf("skipping because this is a PR build")
   511  		os.Exit(0)
   512  	}
   513  	if env.IsCronJob {
   514  		log.Printf("skipping because this is a cron job")
   515  		os.Exit(0)
   516  	}
   517  	if env.Branch != "master" && !strings.HasPrefix(env.Tag, "v1.") {
   518  		log.Printf("skipping because branch %q, tag %q is not on the whitelist", env.Branch, env.Tag)
   519  		os.Exit(0)
   520  	}
   521  }
   522  
   523  // Debian Packaging
   524  
   525  func doDebianSource(cmdline []string) {
   526  	var (
   527  		signer  = flag.String("signer", "", `Signing key name, also used as package author`)
   528  		upload  = flag.String("upload", "", `Where to upload the source package (usually "ppa:klaytn/klaytn")`)
   529  		workdir = flag.String("workdir", "", `Output directory for packages (uses temp dir if unset)`)
   530  		now     = time.Now()
   531  	)
   532  	flag.CommandLine.Parse(cmdline)
   533  	*workdir = makeWorkdir(*workdir)
   534  	env := build.Env()
   535  	maybeSkipArchive(env)
   536  
   537  	// Import the signing key.
   538  	if b64key := os.Getenv("PPA_SIGNING_KEY"); b64key != "" {
   539  		key, err := base64.StdEncoding.DecodeString(b64key)
   540  		if err != nil {
   541  			log.Fatal("invalid base64 PPA_SIGNING_KEY")
   542  		}
   543  		gpg := exec.Command("gpg", "--import")
   544  		gpg.Stdin = bytes.NewReader(key)
   545  		build.MustRun(gpg)
   546  	}
   547  
   548  	// Create the packages.
   549  	for _, distro := range debDistros {
   550  		meta := newDebMetadata(distro, *signer, env, now)
   551  		pkgdir := stageDebianSource(*workdir, meta)
   552  		debuild := exec.Command("debuild", "-S", "-sa", "-us", "-uc")
   553  		debuild.Dir = pkgdir
   554  		build.MustRun(debuild)
   555  
   556  		changes := fmt.Sprintf("%s_%s_source.changes", meta.Name(), meta.VersionString())
   557  		changes = filepath.Join(*workdir, changes)
   558  		if *signer != "" {
   559  			build.MustRunCommand("debsign", changes)
   560  		}
   561  		if *upload != "" {
   562  			build.MustRunCommand("dput", *upload, changes)
   563  		}
   564  	}
   565  }
   566  
   567  func makeWorkdir(wdflag string) string {
   568  	var err error
   569  	if wdflag != "" {
   570  		err = os.MkdirAll(wdflag, 0o744)
   571  	} else {
   572  		wdflag, err = os.MkdirTemp("", "klay-build-")
   573  	}
   574  	if err != nil {
   575  		log.Fatal(err)
   576  	}
   577  	return wdflag
   578  }
   579  
   580  func isUnstableBuild(env build.Environment) bool {
   581  	if env.Tag != "" {
   582  		return false
   583  	}
   584  	return true
   585  }
   586  
   587  type debMetadata struct {
   588  	Env build.Environment
   589  
   590  	// klaytn version being built. Note that this
   591  	// is not the debian package version. The package version
   592  	// is constructed by VersionString.
   593  	Version string
   594  
   595  	Author       string // "name <email>", also selects signing key
   596  	Distro, Time string
   597  	Executables  []debExecutable
   598  }
   599  
   600  type debExecutable struct {
   601  	Name, Description string
   602  }
   603  
   604  func newDebMetadata(distro, author string, env build.Environment, t time.Time) debMetadata {
   605  	if author == "" {
   606  		// No signing key, use default author.
   607  		author = "Klaytn Builds <infra@groundx.xyz>"
   608  	}
   609  	return debMetadata{
   610  		Env:         env,
   611  		Author:      author,
   612  		Distro:      distro,
   613  		Version:     build.VERSION(),
   614  		Time:        t.Format(time.RFC1123Z),
   615  		Executables: debExecutables,
   616  	}
   617  }
   618  
   619  // Name returns the name of the metapackage that depends
   620  // on all executable packages.
   621  func (meta debMetadata) Name() string {
   622  	if isUnstableBuild(meta.Env) {
   623  		return "klaytn-unstable"
   624  	}
   625  	return "klaytn"
   626  }
   627  
   628  // VersionString returns the debian version of the packages.
   629  func (meta debMetadata) VersionString() string {
   630  	vsn := meta.Version
   631  	if meta.Env.Buildnum != "" {
   632  		vsn += "+build" + meta.Env.Buildnum
   633  	}
   634  	if meta.Distro != "" {
   635  		vsn += "+" + meta.Distro
   636  	}
   637  	return vsn
   638  }
   639  
   640  // ExeList returns the list of all executable packages.
   641  func (meta debMetadata) ExeList() string {
   642  	names := make([]string, len(meta.Executables))
   643  	for i, e := range meta.Executables {
   644  		names[i] = meta.ExeName(e)
   645  	}
   646  	return strings.Join(names, ", ")
   647  }
   648  
   649  // ExeName returns the package name of an executable package.
   650  func (meta debMetadata) ExeName(exe debExecutable) string {
   651  	if isUnstableBuild(meta.Env) {
   652  		return exe.Name + "-unstable"
   653  	}
   654  	return exe.Name
   655  }
   656  
   657  // ExeConflicts returns the content of the Conflicts field
   658  // for executable packages.
   659  func (meta debMetadata) ExeConflicts(exe debExecutable) string {
   660  	if isUnstableBuild(meta.Env) {
   661  		// Set up the conflicts list so that the *-unstable packages
   662  		// cannot be installed alongside the regular version.
   663  		//
   664  		// https://www.debian.org/doc/debian-policy/ch-relationships.html
   665  		// is very explicit about Conflicts: and says that Breaks: should
   666  		// be preferred and the conflicting files should be handled via
   667  		// alternates. We might do this eventually but using a conflict is
   668  		// easier now.
   669  		return "klaytn, " + exe.Name
   670  	}
   671  	return ""
   672  }
   673  
   674  func stageDebianSource(tmpdir string, meta debMetadata) (pkgdir string) {
   675  	pkg := meta.Name() + "-" + meta.VersionString()
   676  	pkgdir = filepath.Join(tmpdir, pkg)
   677  	if err := os.Mkdir(pkgdir, 0o755); err != nil {
   678  		log.Fatal(err)
   679  	}
   680  
   681  	// Copy the source code.
   682  	build.MustRunCommand("git", "checkout-index", "-a", "--prefix", pkgdir+string(filepath.Separator))
   683  
   684  	// Put the debian build files in place.
   685  	debian := filepath.Join(pkgdir, "debian")
   686  	build.Render("build/deb.rules", filepath.Join(debian, "rules"), 0o755, meta)
   687  	build.Render("build/deb.changelog", filepath.Join(debian, "changelog"), 0o644, meta)
   688  	build.Render("build/deb.control", filepath.Join(debian, "control"), 0o644, meta)
   689  	build.Render("build/deb.copyright", filepath.Join(debian, "copyright"), 0o644, meta)
   690  	build.RenderString("8\n", filepath.Join(debian, "compat"), 0o644, meta)
   691  	build.RenderString("3.0 (native)\n", filepath.Join(debian, "source/format"), 0o644, meta)
   692  	for _, exe := range meta.Executables {
   693  		install := filepath.Join(debian, meta.ExeName(exe)+".install")
   694  		docs := filepath.Join(debian, meta.ExeName(exe)+".docs")
   695  		build.Render("build/deb.install", install, 0o644, exe)
   696  		build.Render("build/deb.docs", docs, 0o644, exe)
   697  	}
   698  
   699  	return pkgdir
   700  }
   701  
   702  // Windows installer
   703  
   704  func doWindowsInstaller(cmdline []string) {
   705  	// Parse the flags and make skip installer generation on PRs
   706  	var (
   707  		arch    = flag.String("arch", runtime.GOARCH, "Architecture for cross build packaging")
   708  		signer  = flag.String("signer", "", `Environment variable holding the signing key (e.g. WINDOWS_SIGNING_KEY)`)
   709  		upload  = flag.String("upload", "", `Destination to upload the archives`)
   710  		workdir = flag.String("workdir", "", `Output directory for packages (uses temp dir if unset)`)
   711  	)
   712  	flag.CommandLine.Parse(cmdline)
   713  	*workdir = makeWorkdir(*workdir)
   714  	env := build.Env()
   715  	maybeSkipArchive(env)
   716  
   717  	// Aggregate binaries that are included in the installer
   718  	var (
   719  		devTools []string
   720  		allTools []string
   721  		klayTool string
   722  	)
   723  	for _, file := range allToolsArchiveFiles {
   724  		if file == "COPYING" { // license, copied later
   725  			continue
   726  		}
   727  		allTools = append(allTools, filepath.Base(file))
   728  		if filepath.Base(file) == "klay.exe" {
   729  			klayTool = file
   730  		} else {
   731  			devTools = append(devTools, file)
   732  		}
   733  	}
   734  
   735  	// Render NSIS scripts: Installer NSIS contains two installer sections,
   736  	// first section contains the klaytn binary, second section holds the dev tools.
   737  	templateData := map[string]interface{}{
   738  		"License":  "COPYING",
   739  		"Klay":     klayTool,
   740  		"DevTools": devTools,
   741  	}
   742  	build.Render("build/nsis.klay.nsi", filepath.Join(*workdir, "klay.nsi"), 0o644, nil)
   743  	build.Render("build/nsis.install.nsh", filepath.Join(*workdir, "install.nsh"), 0o644, templateData)
   744  	build.Render("build/nsis.uninstall.nsh", filepath.Join(*workdir, "uninstall.nsh"), 0o644, allTools)
   745  	build.Render("build/nsis.pathupdate.nsh", filepath.Join(*workdir, "PathUpdate.nsh"), 0o644, nil)
   746  	build.Render("build/nsis.envvarupdate.nsh", filepath.Join(*workdir, "EnvVarUpdate.nsh"), 0o644, nil)
   747  	build.CopyFile(filepath.Join(*workdir, "SimpleFC.dll"), "build/nsis.simplefc.dll", 0o755)
   748  	build.CopyFile(filepath.Join(*workdir, "COPYING"), "COPYING", 0o755)
   749  
   750  	// Build the installer. This assumes that all the needed files have been previously
   751  	// built (don't mix building and packaging to keep cross compilation complexity to a
   752  	// minimum).
   753  	version := strings.Split(build.VERSION(), ".")
   754  	if env.Commit != "" {
   755  		version[2] += "-" + env.Commit[:8]
   756  	}
   757  	installer, _ := filepath.Abs("klay-" + archiveBasename(*arch, env) + ".exe")
   758  	build.MustRunCommand("makensis.exe",
   759  		"/DOUTPUTFILE="+installer,
   760  		"/DMAJORVERSION="+version[0],
   761  		"/DMINORVERSION="+version[1],
   762  		"/DBUILDVERSION="+version[2],
   763  		"/DARCH="+*arch,
   764  		filepath.Join(*workdir, "klay.nsi"),
   765  	)
   766  
   767  	// Sign and publish installer.
   768  	if err := archiveUpload(installer, *upload, *signer); err != nil {
   769  		log.Fatal(err)
   770  	}
   771  }
   772  
   773  // Android archives
   774  
   775  func doAndroidArchive(cmdline []string) {
   776  	var (
   777  		local  = flag.Bool("local", false, `Flag whether we're only doing a local build (skip Maven artifacts)`)
   778  		signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. ANDROID_SIGNING_KEY)`)
   779  		deploy = flag.String("deploy", "", `Destination to deploy the archive (usually "https://oss.sonatype.org")`)
   780  		upload = flag.String("upload", "", `Destination to upload the archive`)
   781  	)
   782  	flag.CommandLine.Parse(cmdline)
   783  	env := build.Env()
   784  
   785  	// Sanity check that the SDK and NDK are installed and set
   786  	if os.Getenv("ANDROID_HOME") == "" {
   787  		log.Fatal("Please ensure ANDROID_HOME points to your Android SDK")
   788  	}
   789  	if os.Getenv("ANDROID_NDK") == "" {
   790  		log.Fatal("Please ensure ANDROID_NDK points to your Android NDK")
   791  	}
   792  	// Build the Android archive and Maven resources
   793  	build.MustRun(goTool("get", "golang.org/x/mobile/cmd/gomobile", "golang.org/x/mobile/cmd/gobind"))
   794  	build.MustRun(gomobileTool("init", "--ndk", os.Getenv("ANDROID_NDK")))
   795  	build.MustRun(gomobileTool("bind", "-ldflags", "-s -w", "--target", "android", "--javapkg", "org.klaytn", "-v", "github.com/klaytn/klaytn/mobile"))
   796  
   797  	if *local {
   798  		// If we're building locally, copy bundle to build dir and skip Maven
   799  		os.Rename("klay.aar", filepath.Join(GOBIN, "klay.aar"))
   800  		return
   801  	}
   802  	meta := newMavenMetadata(env)
   803  	build.Render("build/mvn.pom", meta.Package+".pom", 0o755, meta)
   804  
   805  	// Skip Maven deploy and Azure upload for PR builds
   806  	maybeSkipArchive(env)
   807  
   808  	// Sign and upload the archive to Azure
   809  	archive := "klay-" + archiveBasename("android", env) + ".aar"
   810  	os.Rename("klay.aar", archive)
   811  
   812  	if err := archiveUpload(archive, *upload, *signer); err != nil {
   813  		log.Fatal(err)
   814  	}
   815  	// Sign and upload all the artifacts to Maven Central
   816  	os.Rename(archive, meta.Package+".aar")
   817  	if *signer != "" && *deploy != "" {
   818  		// Import the signing key into the local GPG instance
   819  		b64key := os.Getenv(*signer)
   820  		key, err := base64.StdEncoding.DecodeString(b64key)
   821  		if err != nil {
   822  			log.Fatalf("invalid base64 %s", *signer)
   823  		}
   824  		gpg := exec.Command("gpg", "--import")
   825  		gpg.Stdin = bytes.NewReader(key)
   826  		build.MustRun(gpg)
   827  
   828  		keyID, err := build.PGPKeyID(string(key))
   829  		if err != nil {
   830  			log.Fatal(err)
   831  		}
   832  		// Upload the artifacts to Sonatype and/or Maven Central
   833  		repo := *deploy + "/service/local/staging/deploy/maven2"
   834  		if meta.Develop {
   835  			repo = *deploy + "/content/repositories/snapshots"
   836  		}
   837  		build.MustRunCommand("mvn", "gpg:sign-and-deploy-file", "-e", "-X",
   838  			"-settings=build/mvn.settings", "-Durl="+repo, "-DrepositoryId=ossrh",
   839  			"-Dgpg.keyname="+keyID,
   840  			"-DpomFile="+meta.Package+".pom", "-Dfile="+meta.Package+".aar")
   841  	}
   842  }
   843  
   844  func gomobileTool(subcmd string, args ...string) *exec.Cmd {
   845  	cmd := exec.Command(filepath.Join(GOBIN, "gomobile"), subcmd)
   846  	cmd.Args = append(cmd.Args, args...)
   847  	cmd.Env = []string{
   848  		"GOPATH=" + build.GOPATH(),
   849  		"PATH=" + GOBIN + string(os.PathListSeparator) + os.Getenv("PATH"),
   850  	}
   851  	for _, e := range os.Environ() {
   852  		if strings.HasPrefix(e, "GOPATH=") || strings.HasPrefix(e, "PATH=") {
   853  			continue
   854  		}
   855  		cmd.Env = append(cmd.Env, e)
   856  	}
   857  	return cmd
   858  }
   859  
   860  type mavenMetadata struct {
   861  	Version      string
   862  	Package      string
   863  	Develop      bool
   864  	Contributors []mavenContributor
   865  }
   866  
   867  type mavenContributor struct {
   868  	Name  string
   869  	Email string
   870  }
   871  
   872  func newMavenMetadata(env build.Environment) mavenMetadata {
   873  	// Collect the list of authors from the repo root
   874  	contribs := []mavenContributor{}
   875  	if authors, err := os.Open("AUTHORS"); err == nil {
   876  		defer authors.Close()
   877  
   878  		scanner := bufio.NewScanner(authors)
   879  		for scanner.Scan() {
   880  			// Skip any whitespace from the authors list
   881  			line := strings.TrimSpace(scanner.Text())
   882  			if line == "" || line[0] == '#' {
   883  				continue
   884  			}
   885  			// Split the author and insert as a contributor
   886  			re := regexp.MustCompile("([^<]+) <(.+)>")
   887  			parts := re.FindStringSubmatch(line)
   888  			if len(parts) == 3 {
   889  				contribs = append(contribs, mavenContributor{Name: parts[1], Email: parts[2]})
   890  			}
   891  		}
   892  	}
   893  	// Render the version and package strings
   894  	version := build.VERSION()
   895  	if isUnstableBuild(env) {
   896  		version += "-SNAPSHOT"
   897  	}
   898  	return mavenMetadata{
   899  		Version:      version,
   900  		Package:      "klay-" + version,
   901  		Develop:      isUnstableBuild(env),
   902  		Contributors: contribs,
   903  	}
   904  }
   905  
   906  // XCode frameworks
   907  
   908  func doXCodeFramework(cmdline []string) {
   909  	var (
   910  		local  = flag.Bool("local", false, `Flag whether we're only doing a local build (skip Maven artifacts)`)
   911  		signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. IOS_SIGNING_KEY)`)
   912  		deploy = flag.String("deploy", "", `Destination to deploy the archive (usually "trunk")`)
   913  		upload = flag.String("upload", "", `Destination to upload the archives`)
   914  	)
   915  	flag.CommandLine.Parse(cmdline)
   916  	env := build.Env()
   917  
   918  	// Build the iOS XCode framework
   919  	build.MustRun(goTool("get", "golang.org/x/mobile/cmd/gomobile", "golang.org/x/mobile/cmd/gobind"))
   920  	build.MustRun(gomobileTool("init"))
   921  	bind := gomobileTool("bind", "-ldflags", "-s -w", "--target", "ios", "-v", "github.com/klaytn/klaytn/mobile")
   922  
   923  	if *local {
   924  		// If we're building locally, use the build folder and stop afterwards
   925  		bind.Dir, _ = filepath.Abs(GOBIN)
   926  		build.MustRun(bind)
   927  		return
   928  	}
   929  	archive := "klay-" + archiveBasename("ios", env)
   930  	if err := os.Mkdir(archive, os.ModePerm); err != nil {
   931  		log.Fatal(err)
   932  	}
   933  	bind.Dir, _ = filepath.Abs(archive)
   934  	build.MustRun(bind)
   935  	build.MustRunCommand("tar", "-zcvf", archive+".tar.gz", archive)
   936  
   937  	// Skip CocoaPods deploy and Azure upload for PR builds
   938  	maybeSkipArchive(env)
   939  
   940  	// Sign and upload the framework to Azure
   941  	if err := archiveUpload(archive+".tar.gz", *upload, *signer); err != nil {
   942  		log.Fatal(err)
   943  	}
   944  	// Prepare and upload a PodSpec to CocoaPods
   945  	if *deploy != "" {
   946  		meta := newPodMetadata(env, archive)
   947  		build.Render("build/pod.podspec", "Klaytn.podspec", 0o755, meta)
   948  		build.MustRunCommand("pod", *deploy, "push", "Klaytn.podspec", "--allow-warnings", "--verbose")
   949  	}
   950  }
   951  
   952  type podMetadata struct {
   953  	Version      string
   954  	Commit       string
   955  	Archive      string
   956  	Contributors []podContributor
   957  }
   958  
   959  type podContributor struct {
   960  	Name  string
   961  	Email string
   962  }
   963  
   964  func newPodMetadata(env build.Environment, archive string) podMetadata {
   965  	// Collect the list of authors from the repo root
   966  	contribs := []podContributor{}
   967  	if authors, err := os.Open("AUTHORS"); err == nil {
   968  		defer authors.Close()
   969  
   970  		scanner := bufio.NewScanner(authors)
   971  		for scanner.Scan() {
   972  			// Skip any whitespace from the authors list
   973  			line := strings.TrimSpace(scanner.Text())
   974  			if line == "" || line[0] == '#' {
   975  				continue
   976  			}
   977  			// Split the author and insert as a contributor
   978  			re := regexp.MustCompile("([^<]+) <(.+)>")
   979  			parts := re.FindStringSubmatch(line)
   980  			if len(parts) == 3 {
   981  				contribs = append(contribs, podContributor{Name: parts[1], Email: parts[2]})
   982  			}
   983  		}
   984  	}
   985  	version := build.VERSION()
   986  	if isUnstableBuild(env) {
   987  		version += "-unstable." + env.Buildnum
   988  	}
   989  	return podMetadata{
   990  		Archive:      archive,
   991  		Version:      version,
   992  		Commit:       env.Commit,
   993  		Contributors: contribs,
   994  	}
   995  }
   996  
   997  func installLinter() string {
   998  	lintBin := filepath.Join(build.GOPATH(), "bin", "golangci-lint")
   999  
  1000  	_, err := exec.LookPath(lintBin)
  1001  	if err != nil {
  1002  		fmt.Println("Installing golangci-lint.")
  1003  
  1004  		cmdCurl := exec.Command("curl", "-sSfL", "https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh")
  1005  		cmdSh := exec.Command("sh", "-s", "--", "-b", filepath.Join(build.GOPATH(), "bin"), "v1.52.0")
  1006  		cmdSh.Stdin, err = cmdCurl.StdoutPipe()
  1007  		if err != nil {
  1008  			log.Fatal(err)
  1009  		}
  1010  
  1011  		fmt.Println(">>>", strings.Join(cmdCurl.Args, " "))
  1012  		if err := cmdCurl.Start(); err != nil {
  1013  			log.Fatal(err)
  1014  		}
  1015  
  1016  		build.MustRun(cmdSh)
  1017  
  1018  		if err := cmdCurl.Wait(); err != nil {
  1019  			log.Fatal(err)
  1020  		}
  1021  	}
  1022  	return lintBin
  1023  }