blob: d810726eb432cfed9511fb34d349cba72ebf479a [file] [log] [blame]
Chris Parsonsf3c96ef2020-09-29 02:23:17 -04001// Copyright 2020 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 android
16
17import (
18 "bytes"
19 "errors"
20 "fmt"
Chris Parsonsa798d962020-10-12 23:44:08 -040021 "io/ioutil"
Chris Parsonsf3c96ef2020-09-29 02:23:17 -040022 "os"
23 "os/exec"
Chris Parsonsa798d962020-10-12 23:44:08 -040024 "path/filepath"
Chris Parsonsf3c96ef2020-09-29 02:23:17 -040025 "runtime"
26 "strings"
27 "sync"
Chris Parsonsa798d962020-10-12 23:44:08 -040028
Patrice Arruda05ab2d02020-12-12 06:24:26 +000029 "android/soong/bazel"
30 "android/soong/shared"
Chris Parsonsa798d962020-10-12 23:44:08 -040031 "github.com/google/blueprint/bootstrap"
Chris Parsonsf3c96ef2020-09-29 02:23:17 -040032)
33
Chris Parsonsb0f8ac42020-10-23 16:48:08 -040034type CqueryRequestType int
35
36const (
37 getAllFiles CqueryRequestType = iota
38)
39
Chris Parsonsf3c96ef2020-09-29 02:23:17 -040040// Map key to describe bazel cquery requests.
41type cqueryKey struct {
Chris Parsonsb0f8ac42020-10-23 16:48:08 -040042 label string
43 requestType CqueryRequestType
Chris Parsonsf3c96ef2020-09-29 02:23:17 -040044}
45
46type BazelContext interface {
47 // The below methods involve queuing cquery requests to be later invoked
48 // by bazel. If any of these methods return (_, false), then the request
49 // has been queued to be run later.
50
51 // Returns result files built by building the given bazel target label.
52 GetAllFiles(label string) ([]string, bool)
53
54 // TODO(cparsons): Other cquery-related methods should be added here.
55 // ** End cquery methods
56
57 // Issues commands to Bazel to receive results for all cquery requests
58 // queued in the BazelContext.
59 InvokeBazel() error
60
61 // Returns true if bazel is enabled for the given configuration.
62 BazelEnabled() bool
63}
64
65// A context object which tracks queued requests that need to be made to Bazel,
66// and their results after the requests have been made.
67type bazelContext struct {
68 homeDir string
69 bazelPath string
70 outputBase string
71 workspaceDir string
Chris Parsonsb0f8ac42020-10-23 16:48:08 -040072 buildDir string
Patrice Arruda05ab2d02020-12-12 06:24:26 +000073 metricsDir string
Chris Parsonsf3c96ef2020-09-29 02:23:17 -040074
75 requests map[cqueryKey]bool // cquery requests that have not yet been issued to Bazel
76 requestMutex sync.Mutex // requests can be written in parallel
77
78 results map[cqueryKey]string // Results of cquery requests after Bazel invocations
79}
80
81var _ BazelContext = &bazelContext{}
82
83// A bazel context to use when Bazel is disabled.
84type noopBazelContext struct{}
85
86var _ BazelContext = noopBazelContext{}
87
88// A bazel context to use for tests.
89type MockBazelContext struct {
90 AllFiles map[string][]string
91}
92
93func (m MockBazelContext) GetAllFiles(label string) ([]string, bool) {
94 result, ok := m.AllFiles[label]
95 return result, ok
96}
97
98func (m MockBazelContext) InvokeBazel() error {
99 panic("unimplemented")
100}
101
102func (m MockBazelContext) BazelEnabled() bool {
103 return true
104}
105
106var _ BazelContext = MockBazelContext{}
107
108func (bazelCtx *bazelContext) GetAllFiles(label string) ([]string, bool) {
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400109 result, ok := bazelCtx.cquery(label, getAllFiles)
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400110 if ok {
111 bazelOutput := strings.TrimSpace(result)
112 return strings.Split(bazelOutput, ", "), true
113 } else {
114 return nil, false
115 }
116}
117
118func (n noopBazelContext) GetAllFiles(label string) ([]string, bool) {
119 panic("unimplemented")
120}
121
122func (n noopBazelContext) InvokeBazel() error {
123 panic("unimplemented")
124}
125
126func (n noopBazelContext) BazelEnabled() bool {
127 return false
128}
129
130func NewBazelContext(c *config) (BazelContext, error) {
Chris Parsons8b77a002020-10-27 18:59:25 -0400131 // TODO(cparsons): Assess USE_BAZEL=1 instead once "mixed Soong/Bazel builds"
132 // are production ready.
133 if c.Getenv("USE_BAZEL_ANALYSIS") != "1" {
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400134 return noopBazelContext{}, nil
135 }
136
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400137 bazelCtx := bazelContext{buildDir: c.buildDir, requests: make(map[cqueryKey]bool)}
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400138 missingEnvVars := []string{}
139 if len(c.Getenv("BAZEL_HOME")) > 1 {
140 bazelCtx.homeDir = c.Getenv("BAZEL_HOME")
141 } else {
142 missingEnvVars = append(missingEnvVars, "BAZEL_HOME")
143 }
144 if len(c.Getenv("BAZEL_PATH")) > 1 {
145 bazelCtx.bazelPath = c.Getenv("BAZEL_PATH")
146 } else {
147 missingEnvVars = append(missingEnvVars, "BAZEL_PATH")
148 }
149 if len(c.Getenv("BAZEL_OUTPUT_BASE")) > 1 {
150 bazelCtx.outputBase = c.Getenv("BAZEL_OUTPUT_BASE")
151 } else {
152 missingEnvVars = append(missingEnvVars, "BAZEL_OUTPUT_BASE")
153 }
154 if len(c.Getenv("BAZEL_WORKSPACE")) > 1 {
155 bazelCtx.workspaceDir = c.Getenv("BAZEL_WORKSPACE")
156 } else {
157 missingEnvVars = append(missingEnvVars, "BAZEL_WORKSPACE")
158 }
Patrice Arruda05ab2d02020-12-12 06:24:26 +0000159 if len(c.Getenv("BAZEL_METRICS_DIR")) > 1 {
160 bazelCtx.metricsDir = c.Getenv("BAZEL_METRICS_DIR")
161 } else {
162 missingEnvVars = append(missingEnvVars, "BAZEL_METRICS_DIR")
163 }
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400164 if len(missingEnvVars) > 0 {
165 return nil, errors.New(fmt.Sprintf("missing required env vars to use bazel: %s", missingEnvVars))
166 } else {
167 return &bazelCtx, nil
168 }
169}
170
Patrice Arruda05ab2d02020-12-12 06:24:26 +0000171func (context *bazelContext) BazelMetricsDir() string {
172 return context.metricsDir
173}
174
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400175func (context *bazelContext) BazelEnabled() bool {
176 return true
177}
178
179// Adds a cquery request to the Bazel request queue, to be later invoked, or
180// returns the result of the given request if the request was already made.
181// If the given request was already made (and the results are available), then
182// returns (result, true). If the request is queued but no results are available,
183// then returns ("", false).
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400184func (context *bazelContext) cquery(label string, requestType CqueryRequestType) (string, bool) {
185 key := cqueryKey{label, requestType}
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400186 if result, ok := context.results[key]; ok {
187 return result, true
188 } else {
189 context.requestMutex.Lock()
190 defer context.requestMutex.Unlock()
191 context.requests[key] = true
192 return "", false
193 }
194}
195
196func pwdPrefix() string {
197 // Darwin doesn't have /proc
198 if runtime.GOOS != "darwin" {
199 return "PWD=/proc/self/cwd"
200 }
201 return ""
202}
203
Patrice Arruda05ab2d02020-12-12 06:24:26 +0000204func (context *bazelContext) issueBazelCommand(runName bazel.RunName, command string, labels []string,
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400205 extraFlags ...string) (string, error) {
206
Jingwen Chen7c6089a2020-11-02 02:56:20 -0500207 cmdFlags := []string{"--output_base=" + context.outputBase, command}
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400208 cmdFlags = append(cmdFlags, labels...)
Chris Parsons8ccdb632020-11-17 15:41:01 -0500209 cmdFlags = append(cmdFlags, "--package_path=%workspace%/"+context.intermediatesDir())
Patrice Arruda05ab2d02020-12-12 06:24:26 +0000210 cmdFlags = append(cmdFlags, "--profile="+shared.BazelMetricsFilename(context, runName))
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400211 cmdFlags = append(cmdFlags, extraFlags...)
212
213 bazelCmd := exec.Command(context.bazelPath, cmdFlags...)
214 bazelCmd.Dir = context.workspaceDir
215 bazelCmd.Env = append(os.Environ(), "HOME="+context.homeDir, pwdPrefix())
216
Colin Crossff0278b2020-10-09 19:24:15 -0700217 stderr := &bytes.Buffer{}
218 bazelCmd.Stderr = stderr
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400219
220 if output, err := bazelCmd.Output(); err != nil {
221 return "", fmt.Errorf("bazel command failed. command: [%s], error [%s]", bazelCmd, stderr)
222 } else {
223 return string(output), nil
224 }
225}
226
Chris Parsons8ccdb632020-11-17 15:41:01 -0500227// Returns the string contents of a workspace file that should be output
228// adjacent to the main bzl file and build file.
229// This workspace file allows, via local_repository rule, sourcetree-level
230// BUILD targets to be referenced via @sourceroot.
231func (context *bazelContext) workspaceFileContents() []byte {
232 formatString := `
233# This file is generated by soong_build. Do not edit.
234local_repository(
235 name = "sourceroot",
236 path = "%s",
237)
238`
239 return []byte(fmt.Sprintf(formatString, context.workspaceDir))
240}
241
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400242func (context *bazelContext) mainBzlFileContents() []byte {
243 contents := `
244# This file is generated by soong_build. Do not edit.
245def _mixed_build_root_impl(ctx):
246 return [DefaultInfo(files = depset(ctx.files.deps))]
247
248mixed_build_root = rule(
249 implementation = _mixed_build_root_impl,
250 attrs = {"deps" : attr.label_list()},
251)
252`
253 return []byte(contents)
254}
255
Chris Parsons8ccdb632020-11-17 15:41:01 -0500256// Returns a "canonicalized" corresponding to the given sourcetree-level label.
257// This abstraction is required because a sourcetree label such as //foo/bar:baz
258// must be referenced via the local repository prefix, such as
259// @sourceroot//foo/bar:baz.
260func canonicalizeLabel(label string) string {
261 if strings.HasPrefix(label, "//") {
262 return "@sourceroot" + label
263 } else {
264 return "@sourceroot//" + label
265 }
266}
267
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400268func (context *bazelContext) mainBuildFileContents() []byte {
269 formatString := `
270# This file is generated by soong_build. Do not edit.
271load(":main.bzl", "mixed_build_root")
272
273mixed_build_root(name = "buildroot",
274 deps = [%s],
275)
276`
277 var buildRootDeps []string = nil
278 for val, _ := range context.requests {
Chris Parsons8ccdb632020-11-17 15:41:01 -0500279 buildRootDeps = append(buildRootDeps, fmt.Sprintf("\"%s\"", canonicalizeLabel(val.label)))
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400280 }
281 buildRootDepsString := strings.Join(buildRootDeps, ",\n ")
282
283 return []byte(fmt.Sprintf(formatString, buildRootDepsString))
284}
285
286func (context *bazelContext) cqueryStarlarkFileContents() []byte {
287 formatString := `
288# This file is generated by soong_build. Do not edit.
289getAllFilesLabels = {
290 %s
291}
292
293def format(target):
294 if str(target.label) in getAllFilesLabels:
295 return str(target.label) + ">>" + ', '.join([f.path for f in target.files.to_list()])
296 else:
297 # This target was not requested via cquery, and thus must be a dependency
298 # of a requested target.
299 return ""
300`
301 var buildRootDeps []string = nil
302 // TODO(cparsons): Sort by request type instead of assuming all requests
303 // are of GetAllFiles type.
304 for val, _ := range context.requests {
Chris Parsons8ccdb632020-11-17 15:41:01 -0500305 buildRootDeps = append(buildRootDeps, fmt.Sprintf("\"%s\" : True", canonicalizeLabel(val.label)))
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400306 }
307 buildRootDepsString := strings.Join(buildRootDeps, ",\n ")
308
309 return []byte(fmt.Sprintf(formatString, buildRootDepsString))
310}
311
Chris Parsons8ccdb632020-11-17 15:41:01 -0500312// Returns a workspace-relative path containing build-related metadata required
313// for interfacing with Bazel. Example: out/soong/bazel.
314func (context *bazelContext) intermediatesDir() string {
315 return filepath.Join(context.buildDir, "bazel")
316}
317
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400318// Issues commands to Bazel to receive results for all cquery requests
319// queued in the BazelContext.
320func (context *bazelContext) InvokeBazel() error {
321 context.results = make(map[cqueryKey]string)
322
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400323 var cqueryOutput string
324 var err error
Chris Parsons8ccdb632020-11-17 15:41:01 -0500325
326 err = os.Mkdir(absolutePath(context.intermediatesDir()), 0777)
327 if err != nil {
328 return err
329 }
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400330 err = ioutil.WriteFile(
Chris Parsons8ccdb632020-11-17 15:41:01 -0500331 absolutePath(filepath.Join(context.intermediatesDir(), "main.bzl")),
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400332 context.mainBzlFileContents(), 0666)
333 if err != nil {
334 return err
335 }
336 err = ioutil.WriteFile(
Chris Parsons8ccdb632020-11-17 15:41:01 -0500337 absolutePath(filepath.Join(context.intermediatesDir(), "BUILD.bazel")),
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400338 context.mainBuildFileContents(), 0666)
339 if err != nil {
340 return err
341 }
Chris Parsons8ccdb632020-11-17 15:41:01 -0500342 cquery_file_relpath := filepath.Join(context.intermediatesDir(), "buildroot.cquery")
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400343 err = ioutil.WriteFile(
344 absolutePath(cquery_file_relpath),
345 context.cqueryStarlarkFileContents(), 0666)
346 if err != nil {
347 return err
348 }
Chris Parsons8ccdb632020-11-17 15:41:01 -0500349 workspace_file_relpath := filepath.Join(context.intermediatesDir(), "WORKSPACE.bazel")
350 err = ioutil.WriteFile(
351 absolutePath(workspace_file_relpath),
352 context.workspaceFileContents(), 0666)
353 if err != nil {
354 return err
355 }
356 buildroot_label := "//:buildroot"
Patrice Arruda05ab2d02020-12-12 06:24:26 +0000357 cqueryOutput, err = context.issueBazelCommand(bazel.CqueryBuildRootRunName, "cquery",
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400358 []string{fmt.Sprintf("deps(%s)", buildroot_label)},
359 "--output=starlark",
360 "--starlark:file="+cquery_file_relpath)
361
362 if err != nil {
363 return err
364 }
365
366 cqueryResults := map[string]string{}
367 for _, outputLine := range strings.Split(cqueryOutput, "\n") {
368 if strings.Contains(outputLine, ">>") {
369 splitLine := strings.SplitN(outputLine, ">>", 2)
370 cqueryResults[splitLine[0]] = splitLine[1]
371 }
372 }
373
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400374 for val, _ := range context.requests {
Chris Parsons8ccdb632020-11-17 15:41:01 -0500375 if cqueryResult, ok := cqueryResults[canonicalizeLabel(val.label)]; ok {
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400376 context.results[val] = string(cqueryResult)
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400377 } else {
Chris Parsonsb0f8ac42020-10-23 16:48:08 -0400378 return fmt.Errorf("missing result for bazel target %s", val.label)
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400379 }
380 }
381
382 // Issue a build command.
383 // TODO(cparsons): Invoking bazel execution during soong_build should be avoided;
384 // bazel actions should either be added to the Ninja file and executed later,
385 // or bazel should handle execution.
386 // TODO(cparsons): Use --target_pattern_file to avoid command line limits.
Patrice Arruda05ab2d02020-12-12 06:24:26 +0000387 _, err = context.issueBazelCommand(bazel.BazelBuildPhonyRootRunName, "build", []string{buildroot_label})
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400388
389 if err != nil {
390 return err
391 }
392
393 // Clear requests.
394 context.requests = map[cqueryKey]bool{}
395 return nil
396}
Chris Parsonsa798d962020-10-12 23:44:08 -0400397
398// Singleton used for registering BUILD file ninja dependencies (needed
399// for correctness of builds which use Bazel.
400func BazelSingleton() Singleton {
401 return &bazelSingleton{}
402}
403
404type bazelSingleton struct{}
405
406func (c *bazelSingleton) GenerateBuildActions(ctx SingletonContext) {
407 if ctx.Config().BazelContext.BazelEnabled() {
408 bazelBuildList := absolutePath(filepath.Join(
409 filepath.Dir(bootstrap.ModuleListFile), "bazel.list"))
410 ctx.AddNinjaFileDeps(bazelBuildList)
411
412 data, err := ioutil.ReadFile(bazelBuildList)
413 if err != nil {
414 ctx.Errorf(err.Error())
415 }
416 files := strings.Split(strings.TrimSpace(string(data)), "\n")
417 for _, file := range files {
418 ctx.AddNinjaFileDeps(file)
419 }
420 }
421}