1// Copyright 2017 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package build 16 17import ( 18 "fmt" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "strconv" 23 24 "android/soong/ui/metrics" 25 soong_metrics_proto "android/soong/ui/metrics/metrics_proto" 26 "android/soong/ui/status" 27 28 "android/soong/shared" 29 30 "github.com/google/blueprint" 31 "github.com/google/blueprint/bootstrap" 32 "github.com/google/blueprint/deptools" 33 "github.com/google/blueprint/microfactory" 34 35 "google.golang.org/protobuf/proto" 36) 37 38const ( 39 availableEnvFile = "soong.environment.available" 40 usedEnvFile = "soong.environment.used" 41 42 soongBuildTag = "build" 43 bp2buildTag = "bp2build" 44 jsonModuleGraphTag = "modulegraph" 45 queryviewTag = "queryview" 46 soongDocsTag = "soong_docs" 47 48 // bootstrapEpoch is used to determine if an incremental build is incompatible with the current 49 // version of bootstrap and needs cleaning before continuing the build. Increment this for 50 // incompatible changes, for example when moving the location of the bpglob binary that is 51 // executed during bootstrap before the primary builder has had a chance to update the path. 52 bootstrapEpoch = 1 53) 54 55func writeEnvironmentFile(ctx Context, envFile string, envDeps map[string]string) error { 56 data, err := shared.EnvFileContents(envDeps) 57 if err != nil { 58 return err 59 } 60 61 return ioutil.WriteFile(envFile, data, 0644) 62} 63 64// This uses Android.bp files and various tools to generate <builddir>/build.ninja. 65// 66// However, the execution of <builddir>/build.ninja happens later in 67// build/soong/ui/build/build.go#Build() 68// 69// We want to rely on as few prebuilts as possible, so we need to bootstrap 70// Soong. The process is as follows: 71// 72// 1. We use "Microfactory", a simple tool to compile Go code, to build 73// first itself, then soong_ui from soong_ui.bash. This binary contains 74// parts of soong_build that are needed to build itself. 75// 2. This simplified version of soong_build then reads the Blueprint files 76// that describe itself and emits .bootstrap/build.ninja that describes 77// how to build its full version and use that to produce the final Ninja 78// file Soong emits. 79// 3. soong_ui executes .bootstrap/build.ninja 80// 81// (After this, Kati is executed to parse the Makefiles, but that's not part of 82// bootstrapping Soong) 83 84// A tiny struct used to tell Blueprint that it's in bootstrap mode. It would 85// probably be nicer to use a flag in bootstrap.Args instead. 86type BlueprintConfig struct { 87 toolDir string 88 soongOutDir string 89 outDir string 90 runGoTests bool 91 debugCompilation bool 92 subninjas []string 93 primaryBuilderInvocations []bootstrap.PrimaryBuilderInvocation 94} 95 96func (c BlueprintConfig) HostToolDir() string { 97 return c.toolDir 98} 99 100func (c BlueprintConfig) SoongOutDir() string { 101 return c.soongOutDir 102} 103 104func (c BlueprintConfig) OutDir() string { 105 return c.outDir 106} 107 108func (c BlueprintConfig) RunGoTests() bool { 109 return c.runGoTests 110} 111 112func (c BlueprintConfig) DebugCompilation() bool { 113 return c.debugCompilation 114} 115 116func (c BlueprintConfig) Subninjas() []string { 117 return c.subninjas 118} 119 120func (c BlueprintConfig) PrimaryBuilderInvocations() []bootstrap.PrimaryBuilderInvocation { 121 return c.primaryBuilderInvocations 122} 123 124func environmentArgs(config Config, tag string) []string { 125 return []string{ 126 "--available_env", shared.JoinPath(config.SoongOutDir(), availableEnvFile), 127 "--used_env", config.UsedEnvFile(tag), 128 } 129} 130 131func writeEmptyFile(ctx Context, path string) { 132 err := os.MkdirAll(filepath.Dir(path), 0777) 133 if err != nil { 134 ctx.Fatalf("Failed to create parent directories of empty file '%s': %s", path, err) 135 } 136 137 if exists, err := fileExists(path); err != nil { 138 ctx.Fatalf("Failed to check if file '%s' exists: %s", path, err) 139 } else if !exists { 140 err = ioutil.WriteFile(path, nil, 0666) 141 if err != nil { 142 ctx.Fatalf("Failed to create empty file '%s': %s", path, err) 143 } 144 } 145} 146 147func fileExists(path string) (bool, error) { 148 if _, err := os.Stat(path); os.IsNotExist(err) { 149 return false, nil 150 } else if err != nil { 151 return false, err 152 } 153 return true, nil 154} 155 156func primaryBuilderInvocation( 157 config Config, 158 name string, 159 output string, 160 specificArgs []string, 161 description string) bootstrap.PrimaryBuilderInvocation { 162 commonArgs := make([]string, 0, 0) 163 164 if !config.skipSoongTests { 165 commonArgs = append(commonArgs, "-t") 166 } 167 168 commonArgs = append(commonArgs, "-l", filepath.Join(config.FileListDir(), "Android.bp.list")) 169 invocationEnv := make(map[string]string) 170 debugMode := os.Getenv("SOONG_DELVE") != "" 171 172 if debugMode { 173 commonArgs = append(commonArgs, "--delve_listen", os.Getenv("SOONG_DELVE")) 174 commonArgs = append(commonArgs, "--delve_path", shared.ResolveDelveBinary()) 175 // GODEBUG=asyncpreemptoff=1 disables the preemption of goroutines. This 176 // is useful because the preemption happens by sending SIGURG to the OS 177 // thread hosting the goroutine in question and each signal results in 178 // work that needs to be done by Delve; it uses ptrace to debug the Go 179 // process and the tracer process must deal with every signal (it is not 180 // possible to selectively ignore SIGURG). This makes debugging slower, 181 // sometimes by an order of magnitude depending on luck. 182 // The original reason for adding async preemption to Go is here: 183 // https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md 184 invocationEnv["GODEBUG"] = "asyncpreemptoff=1" 185 } 186 187 allArgs := make([]string, 0, 0) 188 allArgs = append(allArgs, specificArgs...) 189 allArgs = append(allArgs, 190 "--globListDir", name, 191 "--globFile", config.NamedGlobFile(name)) 192 193 allArgs = append(allArgs, commonArgs...) 194 allArgs = append(allArgs, environmentArgs(config, name)...) 195 allArgs = append(allArgs, "Android.bp") 196 197 return bootstrap.PrimaryBuilderInvocation{ 198 Inputs: []string{"Android.bp"}, 199 Outputs: []string{output}, 200 Args: allArgs, 201 Description: description, 202 // NB: Changing the value of this environment variable will not result in a 203 // rebuild. The bootstrap Ninja file will change, but apparently Ninja does 204 // not consider changing the pool specified in a statement a change that's 205 // worth rebuilding for. 206 Console: os.Getenv("SOONG_UNBUFFERED_OUTPUT") == "1", 207 Env: invocationEnv, 208 } 209} 210 211// bootstrapEpochCleanup deletes files used by bootstrap during incremental builds across 212// incompatible changes. Incompatible changes are marked by incrementing the bootstrapEpoch 213// constant. A tree is considered out of date for the current epoch of the 214// .soong.bootstrap.epoch.<epoch> file doesn't exist. 215func bootstrapEpochCleanup(ctx Context, config Config) { 216 epochFile := fmt.Sprintf(".soong.bootstrap.epoch.%d", bootstrapEpoch) 217 epochPath := filepath.Join(config.SoongOutDir(), epochFile) 218 if exists, err := fileExists(epochPath); err != nil { 219 ctx.Fatalf("failed to check if bootstrap epoch file %q exists: %q", epochPath, err) 220 } else if !exists { 221 // The tree is out of date for the current epoch, delete files used by bootstrap 222 // and force the primary builder to rerun. 223 os.Remove(filepath.Join(config.SoongOutDir(), "build.ninja")) 224 for _, globFile := range bootstrapGlobFileList(config) { 225 os.Remove(globFile) 226 } 227 228 // Mark the tree as up to date with the current epoch by writing the epoch marker file. 229 writeEmptyFile(ctx, epochPath) 230 } 231} 232 233func bootstrapGlobFileList(config Config) []string { 234 return []string{ 235 config.NamedGlobFile(soongBuildTag), 236 config.NamedGlobFile(bp2buildTag), 237 config.NamedGlobFile(jsonModuleGraphTag), 238 config.NamedGlobFile(queryviewTag), 239 config.NamedGlobFile(soongDocsTag), 240 } 241} 242 243func bootstrapBlueprint(ctx Context, config Config) { 244 ctx.BeginTrace(metrics.RunSoong, "blueprint bootstrap") 245 defer ctx.EndTrace() 246 247 // Clean up some files for incremental builds across incompatible changes. 248 bootstrapEpochCleanup(ctx, config) 249 250 mainSoongBuildExtraArgs := []string{"-o", config.SoongNinjaFile()} 251 if config.EmptyNinjaFile() { 252 mainSoongBuildExtraArgs = append(mainSoongBuildExtraArgs, "--empty-ninja-file") 253 } 254 255 mainSoongBuildInvocation := primaryBuilderInvocation( 256 config, 257 soongBuildTag, 258 config.SoongNinjaFile(), 259 mainSoongBuildExtraArgs, 260 fmt.Sprintf("analyzing Android.bp files and generating ninja file at %s", config.SoongNinjaFile()), 261 ) 262 263 if config.bazelBuildMode() == mixedBuild { 264 // Mixed builds call Bazel from soong_build and they therefore need the 265 // Bazel workspace to be available. Make that so by adding a dependency on 266 // the bp2build marker file to the action that invokes soong_build . 267 mainSoongBuildInvocation.Inputs = append(mainSoongBuildInvocation.Inputs, 268 config.Bp2BuildMarkerFile()) 269 } 270 271 bp2buildInvocation := primaryBuilderInvocation( 272 config, 273 bp2buildTag, 274 config.Bp2BuildMarkerFile(), 275 []string{ 276 "--bp2build_marker", config.Bp2BuildMarkerFile(), 277 }, 278 fmt.Sprintf("converting Android.bp files to BUILD files at %s/bp2build", config.SoongOutDir()), 279 ) 280 281 jsonModuleGraphInvocation := primaryBuilderInvocation( 282 config, 283 jsonModuleGraphTag, 284 config.ModuleGraphFile(), 285 []string{ 286 "--module_graph_file", config.ModuleGraphFile(), 287 "--module_actions_file", config.ModuleActionsFile(), 288 }, 289 fmt.Sprintf("generating the Soong module graph at %s", config.ModuleGraphFile()), 290 ) 291 292 queryviewDir := filepath.Join(config.SoongOutDir(), "queryview") 293 queryviewInvocation := primaryBuilderInvocation( 294 config, 295 queryviewTag, 296 config.QueryviewMarkerFile(), 297 []string{ 298 "--bazel_queryview_dir", queryviewDir, 299 }, 300 fmt.Sprintf("generating the Soong module graph as a Bazel workspace at %s", queryviewDir), 301 ) 302 303 soongDocsInvocation := primaryBuilderInvocation( 304 config, 305 soongDocsTag, 306 config.SoongDocsHtml(), 307 []string{ 308 "--soong_docs", config.SoongDocsHtml(), 309 }, 310 fmt.Sprintf("generating Soong docs at %s", config.SoongDocsHtml()), 311 ) 312 313 globFiles := []string{ 314 config.NamedGlobFile(soongBuildTag), 315 config.NamedGlobFile(bp2buildTag), 316 config.NamedGlobFile(jsonModuleGraphTag), 317 config.NamedGlobFile(queryviewTag), 318 config.NamedGlobFile(soongDocsTag), 319 } 320 321 // The glob .ninja files are subninja'd. However, they are generated during 322 // the build itself so we write an empty file if the file does not exist yet 323 // so that the subninja doesn't fail on clean builds 324 for _, globFile := range bootstrapGlobFileList(config) { 325 writeEmptyFile(ctx, globFile) 326 } 327 328 var blueprintArgs bootstrap.Args 329 330 blueprintArgs.ModuleListFile = filepath.Join(config.FileListDir(), "Android.bp.list") 331 blueprintArgs.OutFile = shared.JoinPath(config.SoongOutDir(), "bootstrap.ninja") 332 blueprintArgs.EmptyNinjaFile = false 333 334 blueprintCtx := blueprint.NewContext() 335 blueprintCtx.AddIncludeTags(config.GetIncludeTags()...) 336 blueprintCtx.SetIgnoreUnknownModuleTypes(true) 337 blueprintConfig := BlueprintConfig{ 338 soongOutDir: config.SoongOutDir(), 339 toolDir: config.HostToolDir(), 340 outDir: config.OutDir(), 341 runGoTests: !config.skipSoongTests, 342 // If we want to debug soong_build, we need to compile it for debugging 343 debugCompilation: os.Getenv("SOONG_DELVE") != "", 344 subninjas: globFiles, 345 primaryBuilderInvocations: []bootstrap.PrimaryBuilderInvocation{ 346 mainSoongBuildInvocation, 347 bp2buildInvocation, 348 jsonModuleGraphInvocation, 349 queryviewInvocation, 350 soongDocsInvocation}, 351 } 352 353 bootstrapDeps := bootstrap.RunBlueprint(blueprintArgs, bootstrap.DoEverything, blueprintCtx, blueprintConfig) 354 bootstrapDepFile := shared.JoinPath(config.SoongOutDir(), "bootstrap.ninja.d") 355 err := deptools.WriteDepFile(bootstrapDepFile, blueprintArgs.OutFile, bootstrapDeps) 356 if err != nil { 357 ctx.Fatalf("Error writing depfile '%s': %s", bootstrapDepFile, err) 358 } 359} 360 361func checkEnvironmentFile(currentEnv *Environment, envFile string) { 362 getenv := func(k string) string { 363 v, _ := currentEnv.Get(k) 364 return v 365 } 366 367 if stale, _ := shared.StaleEnvFile(envFile, getenv); stale { 368 os.Remove(envFile) 369 } 370} 371 372func runSoong(ctx Context, config Config) { 373 ctx.BeginTrace(metrics.RunSoong, "soong") 374 defer ctx.EndTrace() 375 376 // We have two environment files: .available is the one with every variable, 377 // .used with the ones that were actually used. The latter is used to 378 // determine whether Soong needs to be re-run since why re-run it if only 379 // unused variables were changed? 380 envFile := filepath.Join(config.SoongOutDir(), availableEnvFile) 381 382 buildMode := config.bazelBuildMode() 383 integratedBp2Build := buildMode == mixedBuild 384 385 // This is done unconditionally, but does not take a measurable amount of time 386 bootstrapBlueprint(ctx, config) 387 388 soongBuildEnv := config.Environment().Copy() 389 soongBuildEnv.Set("TOP", os.Getenv("TOP")) 390 // For Bazel mixed builds. 391 soongBuildEnv.Set("BAZEL_PATH", "./tools/bazel") 392 soongBuildEnv.Set("BAZEL_HOME", filepath.Join(config.BazelOutDir(), "bazelhome")) 393 soongBuildEnv.Set("BAZEL_OUTPUT_BASE", filepath.Join(config.BazelOutDir(), "output")) 394 soongBuildEnv.Set("BAZEL_WORKSPACE", absPath(ctx, ".")) 395 soongBuildEnv.Set("BAZEL_METRICS_DIR", config.BazelMetricsDir()) 396 soongBuildEnv.Set("LOG_DIR", config.LogsDir()) 397 398 // For Soong bootstrapping tests 399 if os.Getenv("ALLOW_MISSING_DEPENDENCIES") == "true" { 400 soongBuildEnv.Set("ALLOW_MISSING_DEPENDENCIES", "true") 401 } 402 403 err := writeEnvironmentFile(ctx, envFile, soongBuildEnv.AsMap()) 404 if err != nil { 405 ctx.Fatalf("failed to write environment file %s: %s", envFile, err) 406 } 407 408 func() { 409 ctx.BeginTrace(metrics.RunSoong, "environment check") 410 defer ctx.EndTrace() 411 412 checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(soongBuildTag)) 413 414 if integratedBp2Build || config.Bp2Build() { 415 checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(bp2buildTag)) 416 } 417 418 if config.JsonModuleGraph() { 419 checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(jsonModuleGraphTag)) 420 } 421 422 if config.Queryview() { 423 checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(queryviewTag)) 424 } 425 426 if config.SoongDocs() { 427 checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(soongDocsTag)) 428 } 429 }() 430 431 runMicrofactory(ctx, config, "bpglob", "github.com/google/blueprint/bootstrap/bpglob", 432 map[string]string{"github.com/google/blueprint": "build/blueprint"}) 433 434 ninja := func(name, ninjaFile string, targets ...string) { 435 ctx.BeginTrace(metrics.RunSoong, name) 436 defer ctx.EndTrace() 437 438 fifo := filepath.Join(config.OutDir(), ".ninja_fifo") 439 nr := status.NewNinjaReader(ctx, ctx.Status.StartTool(), fifo) 440 defer nr.Close() 441 442 ninjaArgs := []string{ 443 "-d", "keepdepfile", 444 "-d", "stats", 445 "-o", "usesphonyoutputs=yes", 446 "-o", "preremoveoutputs=yes", 447 "-w", "dupbuild=err", 448 "-w", "outputdir=err", 449 "-w", "missingoutfile=err", 450 "-j", strconv.Itoa(config.Parallel()), 451 "--frontend_file", fifo, 452 "-f", filepath.Join(config.SoongOutDir(), ninjaFile), 453 } 454 455 ninjaArgs = append(ninjaArgs, targets...) 456 cmd := Command(ctx, config, "soong "+name, 457 config.PrebuiltBuildTool("ninja"), ninjaArgs...) 458 459 var ninjaEnv Environment 460 461 // This is currently how the command line to invoke soong_build finds the 462 // root of the source tree and the output root 463 ninjaEnv.Set("TOP", os.Getenv("TOP")) 464 465 cmd.Environment = &ninjaEnv 466 cmd.Sandbox = soongSandbox 467 cmd.RunAndStreamOrFatal() 468 } 469 470 targets := make([]string, 0, 0) 471 472 if config.JsonModuleGraph() { 473 targets = append(targets, config.ModuleGraphFile()) 474 } 475 476 if config.Bp2Build() { 477 targets = append(targets, config.Bp2BuildMarkerFile()) 478 } 479 480 if config.Queryview() { 481 targets = append(targets, config.QueryviewMarkerFile()) 482 } 483 484 if config.SoongDocs() { 485 targets = append(targets, config.SoongDocsHtml()) 486 } 487 488 if config.SoongBuildInvocationNeeded() { 489 // This build generates <builddir>/build.ninja, which is used later by build/soong/ui/build/build.go#Build(). 490 targets = append(targets, config.SoongNinjaFile()) 491 } 492 493 ninja("bootstrap", "bootstrap.ninja", targets...) 494 495 var soongBuildMetrics *soong_metrics_proto.SoongBuildMetrics 496 if shouldCollectBuildSoongMetrics(config) { 497 soongBuildMetrics := loadSoongBuildMetrics(ctx, config) 498 logSoongBuildMetrics(ctx, soongBuildMetrics) 499 } 500 501 distGzipFile(ctx, config, config.SoongNinjaFile(), "soong") 502 503 if !config.SkipKati() { 504 distGzipFile(ctx, config, config.SoongAndroidMk(), "soong") 505 distGzipFile(ctx, config, config.SoongMakeVarsMk(), "soong") 506 } 507 508 if shouldCollectBuildSoongMetrics(config) && ctx.Metrics != nil { 509 ctx.Metrics.SetSoongBuildMetrics(soongBuildMetrics) 510 } 511 if config.JsonModuleGraph() { 512 distGzipFile(ctx, config, config.ModuleGraphFile(), "soong") 513 } 514} 515 516func runMicrofactory(ctx Context, config Config, name string, pkg string, mapping map[string]string) { 517 ctx.BeginTrace(metrics.RunSoong, name) 518 defer ctx.EndTrace() 519 cfg := microfactory.Config{TrimPath: absPath(ctx, ".")} 520 for pkgPrefix, pathPrefix := range mapping { 521 cfg.Map(pkgPrefix, pathPrefix) 522 } 523 524 exePath := filepath.Join(config.SoongOutDir(), name) 525 dir := filepath.Dir(exePath) 526 if err := os.MkdirAll(dir, 0777); err != nil { 527 ctx.Fatalf("cannot create %s: %s", dir, err) 528 } 529 if _, err := microfactory.Build(&cfg, exePath, pkg); err != nil { 530 ctx.Fatalf("failed to build %s: %s", name, err) 531 } 532} 533 534func shouldCollectBuildSoongMetrics(config Config) bool { 535 // Do not collect metrics protobuf if the soong_build binary ran as the 536 // bp2build converter or the JSON graph dump. 537 return config.SoongBuildInvocationNeeded() 538} 539 540func loadSoongBuildMetrics(ctx Context, config Config) *soong_metrics_proto.SoongBuildMetrics { 541 soongBuildMetricsFile := filepath.Join(config.LogsDir(), "soong_build_metrics.pb") 542 buf, err := ioutil.ReadFile(soongBuildMetricsFile) 543 if err != nil { 544 ctx.Fatalf("Failed to load %s: %s", soongBuildMetricsFile, err) 545 } 546 soongBuildMetrics := &soong_metrics_proto.SoongBuildMetrics{} 547 err = proto.Unmarshal(buf, soongBuildMetrics) 548 if err != nil { 549 ctx.Fatalf("Failed to unmarshal %s: %s", soongBuildMetricsFile, err) 550 } 551 return soongBuildMetrics 552} 553 554func logSoongBuildMetrics(ctx Context, metrics *soong_metrics_proto.SoongBuildMetrics) { 555 ctx.Verbosef("soong_build metrics:") 556 ctx.Verbosef(" modules: %v", metrics.GetModules()) 557 ctx.Verbosef(" variants: %v", metrics.GetVariants()) 558 ctx.Verbosef(" max heap size: %v MB", metrics.GetMaxHeapSize()/1e6) 559 ctx.Verbosef(" total allocation count: %v", metrics.GetTotalAllocCount()) 560 ctx.Verbosef(" total allocation size: %v MB", metrics.GetTotalAllocSize()/1e6) 561 562} 563