golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/buildlet/stage0/stage0.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // The stage0 command looks up the buildlet's URL from its environment
     6  // (GCE metadata service, EC2, etc), downloads it, and runs
     7  // it. If not on GCE, such as when in a Linux Docker container being
     8  // developed and tested locally, the stage0 instead looks for the
     9  // META_BUILDLET_BINARY_URL environment to have a URL to the buildlet
    10  // binary.
    11  //
    12  // The stage0 binary is typically baked into the VM or container
    13  // images or manually copied to dedicated once and is typically never
    14  // auto-updated. Changes to this binary should be rare, as it's
    15  // difficult and slow to roll out. Any per-host-type logic to do at
    16  // start-up should be done in x/build/cmd/buildlet instead, which is
    17  // re-downloaded once per build, and rolls out easily.
    18  package main
    19  
    20  import (
    21  	"flag"
    22  	"fmt"
    23  	"log"
    24  	"net/http"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"runtime"
    29  	"time"
    30  
    31  	"cloud.google.com/go/compute/metadata"
    32  	"golang.org/x/build/internal/httpdl"
    33  	"golang.org/x/build/internal/untar"
    34  )
    35  
    36  // This lets us be lazy and put the stage0 start-up in rc.local where
    37  // it might race with the network coming up, rather than write proper
    38  // upstart+systemd+init scripts:
    39  var networkWait = flag.Duration("network-wait", 0, "if zero, a default is used if needed")
    40  
    41  const osArch = runtime.GOOS + "/" + runtime.GOARCH
    42  
    43  const attr = "buildlet-binary-url"
    44  
    45  // untar helper, for the Windows image prep script.
    46  var (
    47  	untarFile    = flag.String("untar-file", "", "if non-empty, tar.gz to untar to --untar-dest-dir")
    48  	untarDestDir = flag.String("untar-dest-dir", "", "destination directory to untar --untar-file to")
    49  )
    50  
    51  // configureSerialLogOutput and closeSerialLogOutput are set non-nil
    52  // on some platforms to configure log output to go to the serial
    53  // console and to close the serial port, respectively.
    54  var (
    55  	configureSerialLogOutput func()
    56  	closeSerialLogOutput     func()
    57  )
    58  
    59  var timeStart = time.Now()
    60  
    61  func main() {
    62  	if configureSerialLogOutput != nil {
    63  		configureSerialLogOutput()
    64  	}
    65  	log.SetPrefix("stage0: ")
    66  	flag.Parse()
    67  
    68  	onGCE := metadata.OnGCE()
    69  	if *untarFile != "" {
    70  		log.Printf("running in untar mode, untarring %q to %q", *untarFile, *untarDestDir)
    71  		untarMode()
    72  		log.Printf("done untarring; exiting")
    73  		return
    74  	}
    75  	log.Printf("bootstrap binary running")
    76  
    77  	switch osArch {
    78  	case "linux/arm":
    79  		if onGCE {
    80  			break
    81  		}
    82  		switch env := os.Getenv("GO_BUILDER_ENV"); env {
    83  		case "host-linux-arm-aws":
    84  			// No setup currently.
    85  		default:
    86  			panic(fmt.Sprintf("unknown/unspecified $GO_BUILDER_ENV value %q", env))
    87  		}
    88  	case "linux/arm64":
    89  		if onGCE {
    90  			break
    91  		}
    92  		panic(fmt.Sprintf("unknown/unspecified $GO_BUILDER_ENV value %q", os.Getenv("GO_BUILDER_ENV")))
    93  	}
    94  
    95  	if !awaitNetwork() {
    96  		sleepFatalf("network didn't become reachable")
    97  	}
    98  	timeNetwork := time.Now()
    99  	netDelay := prettyDuration(timeNetwork.Sub(timeStart))
   100  	log.Printf("network up after %v", netDelay)
   101  
   102  	// Note: we name it ".exe" for Windows, but the name also
   103  	// works fine on Linux, etc.
   104  	target := filepath.FromSlash("./buildlet.exe")
   105  	if err := download(target, buildletURL()); err != nil {
   106  		sleepFatalf("Downloading %s: %v", buildletURL(), err)
   107  	}
   108  
   109  	if runtime.GOOS != "windows" {
   110  		if err := os.Chmod(target, 0755); err != nil {
   111  			log.Fatal(err)
   112  		}
   113  	}
   114  	downloadDelay := prettyDuration(time.Since(timeNetwork))
   115  	log.Printf("downloaded buildlet in %v", downloadDelay)
   116  
   117  	env := os.Environ()
   118  	if isUnix() && os.Getuid() == 0 {
   119  		if os.Getenv("USER") == "" {
   120  			env = append(env, "USER=root")
   121  		}
   122  		if os.Getenv("HOME") == "" {
   123  			env = append(env, "HOME=/root")
   124  		}
   125  	}
   126  	env = append(env, fmt.Sprintf("GO_STAGE0_NET_DELAY=%v", netDelay))
   127  	env = append(env, fmt.Sprintf("GO_STAGE0_DL_DELAY=%v", downloadDelay))
   128  
   129  	cmd := exec.Command(target)
   130  	cmd.Stdout = os.Stdout
   131  	cmd.Stderr = os.Stderr
   132  	cmd.Env = env
   133  
   134  	// buildEnv is set by some builders. It's increasingly set by new ones.
   135  	// It predates the buildtype-vs-hosttype split, so the values aren't
   136  	// always host types, but they're often host types. They should probably
   137  	// be host types in the future, or we can introduce GO_BUILD_HOST_TYPE
   138  	// to be explicit and kill off GO_BUILDER_ENV.
   139  	buildEnv := os.Getenv("GO_BUILDER_ENV")
   140  
   141  	switch buildEnv {
   142  	case "host-linux-arm-aws":
   143  		cmd.Args = append(cmd.Args, os.ExpandEnv("--workdir=${WORKDIR}"))
   144  	case "host-linux-loong64-3a5000":
   145  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   146  		cmd.Args = append(cmd.Args, os.ExpandEnv("--workdir=${WORKDIR}"))
   147  	case "host-linux-mips64le-rtrk":
   148  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   149  		cmd.Args = append(cmd.Args, os.ExpandEnv("--workdir=${WORKDIR}"))
   150  		cmd.Args = append(cmd.Args, os.ExpandEnv("--hostname=${GO_BUILDER_ENV}"))
   151  	case "host-linux-mips64-rtrk":
   152  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   153  		cmd.Args = append(cmd.Args, os.ExpandEnv("--workdir=${WORKDIR}"))
   154  		cmd.Args = append(cmd.Args, os.ExpandEnv("--hostname=${GO_BUILDER_ENV}"))
   155  	case "host-linux-ppc64le-power10-osu":
   156  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   157  	case "host-linux-ppc64le-power9-osu":
   158  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   159  	case "host-linux-ppc64le-osu": // power8
   160  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   161  	case "host-linux-ppc64-sid":
   162  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   163  	case "host-linux-ppc64-sid-power10":
   164  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   165  	case "host-linux-amd64-wsl", "host-linux-riscv64-unmatched":
   166  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   167  	case "host-freebsd-riscv64-unmatched":
   168  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(buildEnv)...)
   169  		cmd.Args = append(cmd.Args, os.ExpandEnv("--workdir=${WORKDIR}"))
   170  	}
   171  	switch osArch {
   172  	case "linux/s390x":
   173  		cmd.Args = append(cmd.Args, "--workdir=/data/golang/workdir")
   174  		cmd.Args = append(cmd.Args, reverseHostTypeArgs("host-linux-s390x")...)
   175  	case "linux/arm64":
   176  		if onGCE {
   177  			break
   178  		}
   179  		panic(fmt.Sprintf("unknown/unspecified $GO_BUILDER_ENV value %q", env))
   180  	case "solaris/amd64", "illumos/amd64":
   181  		hostType := buildEnv
   182  		cmd.Args = append(cmd.Args, reverseHostTypeArgs(hostType)...)
   183  	case "windows/arm64":
   184  		switch buildEnv {
   185  		case "host-windows11-arm64-azure":
   186  			hostType := buildEnv
   187  			cmd.Args = append(cmd.Args, reverseHostTypeArgs(hostType)...)
   188  		default:
   189  			panic(fmt.Sprintf("unknown/unspecified $GO_BUILDER_ENV value %q", env))
   190  		}
   191  	}
   192  	// Release the serial port (if we opened it) so the buildlet
   193  	// process can open & write to it. At least on Windows, only
   194  	// one process can have it open.
   195  	if closeSerialLogOutput != nil {
   196  		closeSerialLogOutput()
   197  	}
   198  	err := cmd.Run()
   199  	if err != nil {
   200  		if configureSerialLogOutput != nil {
   201  			configureSerialLogOutput()
   202  		}
   203  		sleepFatalf("Error running buildlet: %v", err)
   204  	}
   205  }
   206  
   207  // reverseHostTypeArgs returns the default arguments for the buildlet
   208  // for the provided host type. (one of the keys of the
   209  // x/build/dashboard.Hosts map)
   210  func reverseHostTypeArgs(hostType string) []string {
   211  	return []string{
   212  		"--halt=false",
   213  		"--reverse-type=" + hostType,
   214  		"--coordinator=farmer.golang.org:443",
   215  	}
   216  }
   217  
   218  // awaitNetwork reports whether the network came up within 30 seconds,
   219  // determined somewhat arbitrarily via a DNS lookup for google.com.
   220  func awaitNetwork() bool {
   221  	timeout := 30 * time.Second
   222  	if runtime.GOOS == "windows" {
   223  		timeout = 5 * time.Minute // empirically slower sometimes?
   224  	}
   225  	if *networkWait != 0 {
   226  		timeout = *networkWait
   227  	}
   228  	deadline := time.Now().Add(timeout)
   229  	var lastSpam time.Time
   230  	log.Printf("waiting for network.")
   231  	for time.Now().Before(deadline) {
   232  		t0 := time.Now()
   233  		if isNetworkUp() {
   234  			return true
   235  		}
   236  		failAfter := time.Since(t0)
   237  		if now := time.Now(); now.After(lastSpam.Add(5 * time.Second)) {
   238  			log.Printf("network still down for %v; probe failure took %v",
   239  				prettyDuration(time.Since(timeStart)),
   240  				prettyDuration(failAfter))
   241  			lastSpam = now
   242  		}
   243  		time.Sleep(1 * time.Second)
   244  	}
   245  	log.Printf("gave up waiting for network")
   246  	return false
   247  }
   248  
   249  // isNetworkUp reports whether the network is up by hitting an
   250  // known-up HTTP server. It might block for a few seconds before
   251  // returning an answer.
   252  func isNetworkUp() bool {
   253  	const probeURL = "http://farmer.golang.org/netcheck" // 404 is fine.
   254  	c := &http.Client{
   255  		Timeout: 5 * time.Second,
   256  		Transport: &http.Transport{
   257  			DisableKeepAlives: true,
   258  			Proxy:             http.ProxyFromEnvironment,
   259  		},
   260  	}
   261  	res, err := c.Get(probeURL)
   262  	if err != nil {
   263  		return false
   264  	}
   265  	res.Body.Close()
   266  	return true
   267  }
   268  
   269  func buildletURL() string {
   270  	if v := os.Getenv("META_BUILDLET_BINARY_URL"); v != "" {
   271  		return v
   272  	}
   273  
   274  	if metadata.OnGCE() {
   275  		v, err := metadata.InstanceAttributeValue(attr)
   276  		if err == nil {
   277  			return v
   278  		}
   279  		sleepFatalf("on GCE, but no META_BUILDLET_BINARY_URL env or instance attribute %q: %v", attr, err)
   280  	}
   281  
   282  	// Fallback:
   283  	return fmt.Sprintf("https://storage.googleapis.com/go-builder-data/buildlet.%s-%s", runtime.GOOS, runtime.GOARCH)
   284  }
   285  
   286  func sleepFatalf(format string, args ...interface{}) {
   287  	log.Printf(format, args...)
   288  	if runtime.GOOS == "windows" {
   289  		log.Printf("(sleeping for 1 minute before failing)")
   290  		time.Sleep(time.Minute) // so user has time to see it in cmd.exe, maybe
   291  	}
   292  	os.Exit(1)
   293  }
   294  
   295  func download(file, url string) error {
   296  	log.Printf("downloading %s to %s ...\n", url, file)
   297  	const maxTry = 3
   298  	var lastErr error
   299  	for try := 1; try <= maxTry; try++ {
   300  		if try > 1 {
   301  			// network should be up by now per awaitNetwork, so just retry
   302  			// shortly a few time on errors.
   303  			time.Sleep(2)
   304  		}
   305  		err := httpdl.Download(file, url)
   306  		if err == nil {
   307  			fi, err := os.Stat(file)
   308  			if err != nil {
   309  				return err
   310  			}
   311  			log.Printf("downloaded %s (%d bytes)", file, fi.Size())
   312  			return nil
   313  		}
   314  		lastErr = err
   315  		log.Printf("try %d/%d download failure: %v", try, maxTry, err)
   316  	}
   317  	return lastErr
   318  }
   319  
   320  func aptGetInstall(pkgs ...string) {
   321  	t0 := time.Now()
   322  	args := append([]string{"--yes", "install"}, pkgs...)
   323  	cmd := exec.Command("apt-get", args...)
   324  	if out, err := cmd.CombinedOutput(); err != nil {
   325  		log.Fatalf("error running apt-get install: %s", out)
   326  	}
   327  	log.Printf("stage0: apt-get installed %q in %v", pkgs, time.Since(t0).Round(time.Second/10))
   328  }
   329  
   330  func initBootstrapDir(destDir, tgzCache string) {
   331  	t0 := time.Now()
   332  	if err := os.MkdirAll(destDir, 0755); err != nil {
   333  		log.Fatal(err)
   334  	}
   335  	latestURL := fmt.Sprintf("https://storage.googleapis.com/go-builder-data/gobootstrap-%s-%s.tar.gz",
   336  		runtime.GOOS, runtime.GOARCH)
   337  	if err := httpdl.Download(tgzCache, latestURL); err != nil {
   338  		log.Fatalf("dowloading %s to %s: %v", latestURL, tgzCache, err)
   339  	}
   340  	log.Printf("synced %s to %s in %v", latestURL, tgzCache, time.Since(t0).Round(time.Second/10))
   341  
   342  	t1 := time.Now()
   343  	// TODO(bradfitz): rewrite this to use Go instead of shelling
   344  	// out to tar? if this ever gets used on platforms besides
   345  	// Unix. For Windows and Plan 9 we bake in the bootstrap
   346  	// tarball into the image anyway. So this works for now.
   347  	// Solaris might require tweaking to use gtar instead or
   348  	// something.
   349  	tar := exec.Command("tar", "zxf", tgzCache)
   350  	tar.Dir = destDir
   351  	out, err := tar.CombinedOutput()
   352  	if err != nil {
   353  		log.Fatalf("error untarring %s to %s: %s", tgzCache, destDir, out)
   354  	}
   355  	log.Printf("untarred %s to %s in %v", tgzCache, destDir, time.Since(t1).Round(time.Second/10))
   356  }
   357  
   358  func isUnix() bool {
   359  	switch runtime.GOOS {
   360  	case "plan9", "windows":
   361  		return false
   362  	}
   363  	return true
   364  }
   365  
   366  func untarMode() {
   367  	if *untarDestDir == "" {
   368  		log.Fatal("--untar-dest-dir must not be empty")
   369  	}
   370  	if fi, err := os.Stat(*untarDestDir); err != nil || !fi.IsDir() {
   371  		if err != nil {
   372  			log.Fatalf("--untar-dest-dir %q: %v", *untarDestDir, err)
   373  		}
   374  		log.Fatalf("--untar-dest-dir %q not a directory.", *untarDestDir)
   375  	}
   376  	f, err := os.Open(*untarFile)
   377  	if err != nil {
   378  		log.Fatal(err)
   379  	}
   380  	defer f.Close()
   381  	if err := untar.Untar(f, *untarDestDir); err != nil {
   382  		log.Fatalf("Untarring %q to %q: %v", *untarFile, *untarDestDir, err)
   383  	}
   384  }
   385  
   386  func prettyDuration(d time.Duration) time.Duration {
   387  	const round = time.Second / 10
   388  	return d / round * round
   389  }