blob: b7cea4be733c2fdd96bd581edbbbeb3be181287c [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"
21 "os"
22 "os/exec"
23 "runtime"
24 "strings"
25 "sync"
26)
27
28// Map key to describe bazel cquery requests.
29type cqueryKey struct {
30 label string
31 starlarkExpr string
32}
33
34type BazelContext interface {
35 // The below methods involve queuing cquery requests to be later invoked
36 // by bazel. If any of these methods return (_, false), then the request
37 // has been queued to be run later.
38
39 // Returns result files built by building the given bazel target label.
40 GetAllFiles(label string) ([]string, bool)
41
42 // TODO(cparsons): Other cquery-related methods should be added here.
43 // ** End cquery methods
44
45 // Issues commands to Bazel to receive results for all cquery requests
46 // queued in the BazelContext.
47 InvokeBazel() error
48
49 // Returns true if bazel is enabled for the given configuration.
50 BazelEnabled() bool
51}
52
53// A context object which tracks queued requests that need to be made to Bazel,
54// and their results after the requests have been made.
55type bazelContext struct {
56 homeDir string
57 bazelPath string
58 outputBase string
59 workspaceDir string
60
61 requests map[cqueryKey]bool // cquery requests that have not yet been issued to Bazel
62 requestMutex sync.Mutex // requests can be written in parallel
63
64 results map[cqueryKey]string // Results of cquery requests after Bazel invocations
65}
66
67var _ BazelContext = &bazelContext{}
68
69// A bazel context to use when Bazel is disabled.
70type noopBazelContext struct{}
71
72var _ BazelContext = noopBazelContext{}
73
74// A bazel context to use for tests.
75type MockBazelContext struct {
76 AllFiles map[string][]string
77}
78
79func (m MockBazelContext) GetAllFiles(label string) ([]string, bool) {
80 result, ok := m.AllFiles[label]
81 return result, ok
82}
83
84func (m MockBazelContext) InvokeBazel() error {
85 panic("unimplemented")
86}
87
88func (m MockBazelContext) BazelEnabled() bool {
89 return true
90}
91
92var _ BazelContext = MockBazelContext{}
93
94func (bazelCtx *bazelContext) GetAllFiles(label string) ([]string, bool) {
95 starlarkExpr := "', '.join([f.path for f in target.files.to_list()])"
96 result, ok := bazelCtx.cquery(label, starlarkExpr)
97 if ok {
98 bazelOutput := strings.TrimSpace(result)
99 return strings.Split(bazelOutput, ", "), true
100 } else {
101 return nil, false
102 }
103}
104
105func (n noopBazelContext) GetAllFiles(label string) ([]string, bool) {
106 panic("unimplemented")
107}
108
109func (n noopBazelContext) InvokeBazel() error {
110 panic("unimplemented")
111}
112
113func (n noopBazelContext) BazelEnabled() bool {
114 return false
115}
116
117func NewBazelContext(c *config) (BazelContext, error) {
118 if c.Getenv("USE_BAZEL") != "1" {
119 return noopBazelContext{}, nil
120 }
121
122 bazelCtx := bazelContext{requests: make(map[cqueryKey]bool)}
123 missingEnvVars := []string{}
124 if len(c.Getenv("BAZEL_HOME")) > 1 {
125 bazelCtx.homeDir = c.Getenv("BAZEL_HOME")
126 } else {
127 missingEnvVars = append(missingEnvVars, "BAZEL_HOME")
128 }
129 if len(c.Getenv("BAZEL_PATH")) > 1 {
130 bazelCtx.bazelPath = c.Getenv("BAZEL_PATH")
131 } else {
132 missingEnvVars = append(missingEnvVars, "BAZEL_PATH")
133 }
134 if len(c.Getenv("BAZEL_OUTPUT_BASE")) > 1 {
135 bazelCtx.outputBase = c.Getenv("BAZEL_OUTPUT_BASE")
136 } else {
137 missingEnvVars = append(missingEnvVars, "BAZEL_OUTPUT_BASE")
138 }
139 if len(c.Getenv("BAZEL_WORKSPACE")) > 1 {
140 bazelCtx.workspaceDir = c.Getenv("BAZEL_WORKSPACE")
141 } else {
142 missingEnvVars = append(missingEnvVars, "BAZEL_WORKSPACE")
143 }
144 if len(missingEnvVars) > 0 {
145 return nil, errors.New(fmt.Sprintf("missing required env vars to use bazel: %s", missingEnvVars))
146 } else {
147 return &bazelCtx, nil
148 }
149}
150
151func (context *bazelContext) BazelEnabled() bool {
152 return true
153}
154
155// Adds a cquery request to the Bazel request queue, to be later invoked, or
156// returns the result of the given request if the request was already made.
157// If the given request was already made (and the results are available), then
158// returns (result, true). If the request is queued but no results are available,
159// then returns ("", false).
160func (context *bazelContext) cquery(label string, starlarkExpr string) (string, bool) {
161 key := cqueryKey{label, starlarkExpr}
162 if result, ok := context.results[key]; ok {
163 return result, true
164 } else {
165 context.requestMutex.Lock()
166 defer context.requestMutex.Unlock()
167 context.requests[key] = true
168 return "", false
169 }
170}
171
172func pwdPrefix() string {
173 // Darwin doesn't have /proc
174 if runtime.GOOS != "darwin" {
175 return "PWD=/proc/self/cwd"
176 }
177 return ""
178}
179
180func (context *bazelContext) issueBazelCommand(command string, labels []string,
181 extraFlags ...string) (string, error) {
182
183 cmdFlags := []string{"--output_base=" + context.outputBase, command}
184 cmdFlags = append(cmdFlags, labels...)
185 cmdFlags = append(cmdFlags, extraFlags...)
186
187 bazelCmd := exec.Command(context.bazelPath, cmdFlags...)
188 bazelCmd.Dir = context.workspaceDir
189 bazelCmd.Env = append(os.Environ(), "HOME="+context.homeDir, pwdPrefix())
190
Colin Crossff0278b2020-10-09 19:24:15 -0700191 stderr := &bytes.Buffer{}
192 bazelCmd.Stderr = stderr
Chris Parsonsf3c96ef2020-09-29 02:23:17 -0400193
194 if output, err := bazelCmd.Output(); err != nil {
195 return "", fmt.Errorf("bazel command failed. command: [%s], error [%s]", bazelCmd, stderr)
196 } else {
197 return string(output), nil
198 }
199}
200
201// Issues commands to Bazel to receive results for all cquery requests
202// queued in the BazelContext.
203func (context *bazelContext) InvokeBazel() error {
204 context.results = make(map[cqueryKey]string)
205
206 var labels []string
207 var cqueryOutput string
208 var err error
209 for val, _ := range context.requests {
210 labels = append(labels, val.label)
211
212 // TODO(cparsons): Combine requests into a batch cquery request.
213 // TODO(cparsons): Use --query_file to avoid command line limits.
214 cqueryOutput, err = context.issueBazelCommand("cquery", []string{val.label},
215 "--output=starlark",
216 "--starlark:expr="+val.starlarkExpr)
217
218 if err != nil {
219 return err
220 } else {
221 context.results[val] = string(cqueryOutput)
222 }
223 }
224
225 // Issue a build command.
226 // TODO(cparsons): Invoking bazel execution during soong_build should be avoided;
227 // bazel actions should either be added to the Ninja file and executed later,
228 // or bazel should handle execution.
229 // TODO(cparsons): Use --target_pattern_file to avoid command line limits.
230 _, err = context.issueBazelCommand("build", labels)
231
232 if err != nil {
233 return err
234 }
235
236 // Clear requests.
237 context.requests = map[cqueryKey]bool{}
238 return nil
239}