| // Copyright 2020 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package android |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "os" |
| "os/exec" |
| "runtime" |
| "strings" |
| "sync" |
| ) |
| |
| // Map key to describe bazel cquery requests. |
| type cqueryKey struct { |
| label string |
| starlarkExpr string |
| } |
| |
| type BazelContext interface { |
| // The below methods involve queuing cquery requests to be later invoked |
| // by bazel. If any of these methods return (_, false), then the request |
| // has been queued to be run later. |
| |
| // Returns result files built by building the given bazel target label. |
| GetAllFiles(label string) ([]string, bool) |
| |
| // TODO(cparsons): Other cquery-related methods should be added here. |
| // ** End cquery methods |
| |
| // Issues commands to Bazel to receive results for all cquery requests |
| // queued in the BazelContext. |
| InvokeBazel() error |
| |
| // Returns true if bazel is enabled for the given configuration. |
| BazelEnabled() bool |
| } |
| |
| // A context object which tracks queued requests that need to be made to Bazel, |
| // and their results after the requests have been made. |
| type bazelContext struct { |
| homeDir string |
| bazelPath string |
| outputBase string |
| workspaceDir string |
| |
| requests map[cqueryKey]bool // cquery requests that have not yet been issued to Bazel |
| requestMutex sync.Mutex // requests can be written in parallel |
| |
| results map[cqueryKey]string // Results of cquery requests after Bazel invocations |
| } |
| |
| var _ BazelContext = &bazelContext{} |
| |
| // A bazel context to use when Bazel is disabled. |
| type noopBazelContext struct{} |
| |
| var _ BazelContext = noopBazelContext{} |
| |
| // A bazel context to use for tests. |
| type MockBazelContext struct { |
| AllFiles map[string][]string |
| } |
| |
| func (m MockBazelContext) GetAllFiles(label string) ([]string, bool) { |
| result, ok := m.AllFiles[label] |
| return result, ok |
| } |
| |
| func (m MockBazelContext) InvokeBazel() error { |
| panic("unimplemented") |
| } |
| |
| func (m MockBazelContext) BazelEnabled() bool { |
| return true |
| } |
| |
| var _ BazelContext = MockBazelContext{} |
| |
| func (bazelCtx *bazelContext) GetAllFiles(label string) ([]string, bool) { |
| starlarkExpr := "', '.join([f.path for f in target.files.to_list()])" |
| result, ok := bazelCtx.cquery(label, starlarkExpr) |
| if ok { |
| bazelOutput := strings.TrimSpace(result) |
| return strings.Split(bazelOutput, ", "), true |
| } else { |
| return nil, false |
| } |
| } |
| |
| func (n noopBazelContext) GetAllFiles(label string) ([]string, bool) { |
| panic("unimplemented") |
| } |
| |
| func (n noopBazelContext) InvokeBazel() error { |
| panic("unimplemented") |
| } |
| |
| func (n noopBazelContext) BazelEnabled() bool { |
| return false |
| } |
| |
| func NewBazelContext(c *config) (BazelContext, error) { |
| if c.Getenv("USE_BAZEL") != "1" { |
| return noopBazelContext{}, nil |
| } |
| |
| bazelCtx := bazelContext{requests: make(map[cqueryKey]bool)} |
| missingEnvVars := []string{} |
| if len(c.Getenv("BAZEL_HOME")) > 1 { |
| bazelCtx.homeDir = c.Getenv("BAZEL_HOME") |
| } else { |
| missingEnvVars = append(missingEnvVars, "BAZEL_HOME") |
| } |
| if len(c.Getenv("BAZEL_PATH")) > 1 { |
| bazelCtx.bazelPath = c.Getenv("BAZEL_PATH") |
| } else { |
| missingEnvVars = append(missingEnvVars, "BAZEL_PATH") |
| } |
| if len(c.Getenv("BAZEL_OUTPUT_BASE")) > 1 { |
| bazelCtx.outputBase = c.Getenv("BAZEL_OUTPUT_BASE") |
| } else { |
| missingEnvVars = append(missingEnvVars, "BAZEL_OUTPUT_BASE") |
| } |
| if len(c.Getenv("BAZEL_WORKSPACE")) > 1 { |
| bazelCtx.workspaceDir = c.Getenv("BAZEL_WORKSPACE") |
| } else { |
| missingEnvVars = append(missingEnvVars, "BAZEL_WORKSPACE") |
| } |
| if len(missingEnvVars) > 0 { |
| return nil, errors.New(fmt.Sprintf("missing required env vars to use bazel: %s", missingEnvVars)) |
| } else { |
| return &bazelCtx, nil |
| } |
| } |
| |
| func (context *bazelContext) BazelEnabled() bool { |
| return true |
| } |
| |
| // Adds a cquery request to the Bazel request queue, to be later invoked, or |
| // returns the result of the given request if the request was already made. |
| // If the given request was already made (and the results are available), then |
| // returns (result, true). If the request is queued but no results are available, |
| // then returns ("", false). |
| func (context *bazelContext) cquery(label string, starlarkExpr string) (string, bool) { |
| key := cqueryKey{label, starlarkExpr} |
| if result, ok := context.results[key]; ok { |
| return result, true |
| } else { |
| context.requestMutex.Lock() |
| defer context.requestMutex.Unlock() |
| context.requests[key] = true |
| return "", false |
| } |
| } |
| |
| func pwdPrefix() string { |
| // Darwin doesn't have /proc |
| if runtime.GOOS != "darwin" { |
| return "PWD=/proc/self/cwd" |
| } |
| return "" |
| } |
| |
| func (context *bazelContext) issueBazelCommand(command string, labels []string, |
| extraFlags ...string) (string, error) { |
| |
| cmdFlags := []string{"--output_base=" + context.outputBase, command} |
| cmdFlags = append(cmdFlags, labels...) |
| cmdFlags = append(cmdFlags, extraFlags...) |
| |
| bazelCmd := exec.Command(context.bazelPath, cmdFlags...) |
| bazelCmd.Dir = context.workspaceDir |
| bazelCmd.Env = append(os.Environ(), "HOME="+context.homeDir, pwdPrefix()) |
| |
| stderr := &bytes.Buffer{} |
| bazelCmd.Stderr = stderr |
| |
| if output, err := bazelCmd.Output(); err != nil { |
| return "", fmt.Errorf("bazel command failed. command: [%s], error [%s]", bazelCmd, stderr) |
| } else { |
| return string(output), nil |
| } |
| } |
| |
| // Issues commands to Bazel to receive results for all cquery requests |
| // queued in the BazelContext. |
| func (context *bazelContext) InvokeBazel() error { |
| context.results = make(map[cqueryKey]string) |
| |
| var labels []string |
| var cqueryOutput string |
| var err error |
| for val, _ := range context.requests { |
| labels = append(labels, val.label) |
| |
| // TODO(cparsons): Combine requests into a batch cquery request. |
| // TODO(cparsons): Use --query_file to avoid command line limits. |
| cqueryOutput, err = context.issueBazelCommand("cquery", []string{val.label}, |
| "--output=starlark", |
| "--starlark:expr="+val.starlarkExpr) |
| |
| if err != nil { |
| return err |
| } else { |
| context.results[val] = string(cqueryOutput) |
| } |
| } |
| |
| // Issue a build command. |
| // TODO(cparsons): Invoking bazel execution during soong_build should be avoided; |
| // bazel actions should either be added to the Ninja file and executed later, |
| // or bazel should handle execution. |
| // TODO(cparsons): Use --target_pattern_file to avoid command line limits. |
| _, err = context.issueBazelCommand("build", labels) |
| |
| if err != nil { |
| return err |
| } |
| |
| // Clear requests. |
| context.requests = map[cqueryKey]bool{} |
| return nil |
| } |