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 }