Files
build/tools/ide_query/ide_query.go
Kadir Çetinkaya 874e12be43 Make sure ninja keeps going after errors
Language services builds are usually executed in a non-building state of the codebase as the user is actively editing the code.

This change ensures we can build as many dependencies as possible for language services by ninja.

Note that this doesn't change the error-limit for soong itself, i.e. we can still fail-early if we encounter errors while generating ninja files.

Change-Id: I594e2ae4591d2e75db082486f28f705833ceb4a8
2024-03-15 07:27:30 +00:00

392 lines
10 KiB
Go

/*
* Copyright (C) 2024 The Android Open Source Project
*
* 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.
*/
// Binary ide_query generates and analyzes build artifacts.
// The produced result can be consumed by IDEs to provide language features.
package main
import (
"bytes"
"container/list"
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path"
"slices"
"strings"
"google.golang.org/protobuf/proto"
pb "ide_query/ide_query_proto"
)
// Env contains information about the current environment.
type Env struct {
LunchTarget LunchTarget
RepoDir string
OutDir string
ClangToolsRoot string
CcFiles []string
JavaFiles []string
}
// LunchTarget is a parsed Android lunch target.
// Input format: <product_name>-<release_type>-<build_variant>
type LunchTarget struct {
Product string
Release string
Variant string
}
var _ flag.Value = (*LunchTarget)(nil)
// // Get implements flag.Value.
// func (l *LunchTarget) Get() any {
// return l
// }
// Set implements flag.Value.
func (l *LunchTarget) Set(s string) error {
parts := strings.Split(s, "-")
if len(parts) != 3 {
return fmt.Errorf("invalid lunch target: %q, must have form <product_name>-<release_type>-<build_variant>", s)
}
*l = LunchTarget{
Product: parts[0],
Release: parts[1],
Variant: parts[2],
}
return nil
}
// String implements flag.Value.
func (l *LunchTarget) String() string {
return fmt.Sprintf("%s-%s-%s", l.Product, l.Release, l.Variant)
}
func main() {
var env Env
env.OutDir = os.Getenv("OUT_DIR")
env.RepoDir = os.Getenv("ANDROID_BUILD_TOP")
env.ClangToolsRoot = os.Getenv("PREBUILTS_CLANG_TOOLS_ROOT")
flag.Var(&env.LunchTarget, "lunch_target", "The lunch target to query")
flag.Parse()
files := flag.Args()
if len(files) == 0 {
fmt.Println("No files provided.")
os.Exit(1)
return
}
for _, f := range files {
switch {
case strings.HasSuffix(f, ".java") || strings.HasSuffix(f, ".kt"):
env.JavaFiles = append(env.JavaFiles, f)
case strings.HasSuffix(f, ".cc") || strings.HasSuffix(f, ".cpp") || strings.HasSuffix(f, ".h"):
env.CcFiles = append(env.CcFiles, f)
default:
log.Printf("File %q is supported - will be skipped.", f)
}
}
ctx := context.Background()
// TODO(michaelmerg): Figure out if module_bp_java_deps.json and compile_commands.json is outdated.
runMake(ctx, env, "nothing")
javaModules, javaFileToModuleMap, err := loadJavaModules(&env)
if err != nil {
log.Printf("Failed to load java modules: %v", err)
}
toMake := getJavaTargets(javaFileToModuleMap)
ccTargets, status := getCCTargets(ctx, &env)
if status != nil && status.Code != pb.Status_OK {
log.Fatalf("Failed to query cc targets: %v", *status.Message)
}
toMake = append(toMake, ccTargets...)
fmt.Printf("Running make for modules: %v\n", strings.Join(toMake, ", "))
if err := runMake(ctx, env, toMake...); err != nil {
log.Printf("Building deps failed: %v", err)
}
res := getJavaInputs(&env, javaModules, javaFileToModuleMap)
ccAnalysis := getCCInputs(ctx, &env)
proto.Merge(res, ccAnalysis)
res.BuildArtifactRoot = env.OutDir
data, err := proto.Marshal(res)
if err != nil {
log.Fatalf("Failed to marshal result proto: %v", err)
}
err = os.WriteFile(path.Join(env.RepoDir, env.OutDir, "ide_query.pb"), data, 0644)
if err != nil {
log.Fatalf("Failed to write result proto: %v", err)
}
for _, s := range res.Sources {
fmt.Printf("%s: %v (Deps: %d, Generated: %d)\n", s.GetPath(), s.GetStatus(), len(s.GetDeps()), len(s.GetGenerated()))
}
}
func repoState(env *Env) *pb.RepoState {
const compDbPath = "soong/development/ide/compdb/compile_commands.json"
return &pb.RepoState{
RepoDir: env.RepoDir,
ActiveFilePath: env.CcFiles,
OutDir: env.OutDir,
CompDbPath: path.Join(env.OutDir, compDbPath),
}
}
func runCCanalyzer(ctx context.Context, env *Env, mode string, in []byte) ([]byte, error) {
ccAnalyzerPath := path.Join(env.ClangToolsRoot, "bin/ide_query_cc_analyzer")
outBuffer := new(bytes.Buffer)
inBuffer := new(bytes.Buffer)
inBuffer.Write(in)
cmd := exec.CommandContext(ctx, ccAnalyzerPath, "--mode="+mode)
cmd.Dir = env.RepoDir
cmd.Stdin = inBuffer
cmd.Stdout = outBuffer
cmd.Stderr = os.Stderr
err := cmd.Run()
return outBuffer.Bytes(), err
}
// Execute cc_analyzer and get all the targets that needs to be build for analyzing files.
func getCCTargets(ctx context.Context, env *Env) ([]string, *pb.Status) {
state := repoState(env)
bytes, err := proto.Marshal(state)
if err != nil {
log.Fatalln("Failed to serialize state:", err)
}
resp := new(pb.DepsResponse)
result, err := runCCanalyzer(ctx, env, "deps", bytes)
if marshal_err := proto.Unmarshal(result, resp); marshal_err != nil {
return nil, &pb.Status{
Code: pb.Status_FAILURE,
Message: proto.String("Malformed response from cc_analyzer: " + marshal_err.Error()),
}
}
var targets []string
if resp.Status != nil && resp.Status.Code != pb.Status_OK {
return targets, resp.Status
}
for _, deps := range resp.Deps {
targets = append(targets, deps.BuildTarget...)
}
status := &pb.Status{Code: pb.Status_OK}
if err != nil {
status = &pb.Status{
Code: pb.Status_FAILURE,
Message: proto.String(err.Error()),
}
}
return targets, status
}
func getCCInputs(ctx context.Context, env *Env) *pb.IdeAnalysis {
state := repoState(env)
bytes, err := proto.Marshal(state)
if err != nil {
log.Fatalln("Failed to serialize state:", err)
}
resp := new(pb.IdeAnalysis)
result, err := runCCanalyzer(ctx, env, "inputs", bytes)
if marshal_err := proto.Unmarshal(result, resp); marshal_err != nil {
resp.Status = &pb.Status{
Code: pb.Status_FAILURE,
Message: proto.String("Malformed response from cc_analyzer: " + marshal_err.Error()),
}
return resp
}
if err != nil && (resp.Status == nil || resp.Status.Code == pb.Status_OK) {
resp.Status = &pb.Status{
Code: pb.Status_FAILURE,
Message: proto.String(err.Error()),
}
}
return resp
}
func getJavaTargets(javaFileToModuleMap map[string]*javaModule) []string {
var targets []string
for _, m := range javaFileToModuleMap {
targets = append(targets, m.Name)
}
return targets
}
func getJavaInputs(env *Env, javaModules map[string]*javaModule, javaFileToModuleMap map[string]*javaModule) *pb.IdeAnalysis {
var sources []*pb.SourceFile
type depsAndGenerated struct {
Deps []string
Generated []*pb.GeneratedFile
}
moduleToDeps := make(map[string]*depsAndGenerated)
for _, f := range env.JavaFiles {
file := &pb.SourceFile{
Path: f,
}
sources = append(sources, file)
m := javaFileToModuleMap[f]
if m == nil {
file.Status = &pb.Status{
Code: pb.Status_FAILURE,
Message: proto.String("File not found in any module."),
}
continue
}
file.Status = &pb.Status{Code: pb.Status_OK}
if moduleToDeps[m.Name] != nil {
file.Generated = moduleToDeps[m.Name].Generated
file.Deps = moduleToDeps[m.Name].Deps
continue
}
deps := transitiveDeps(m, javaModules)
var generated []*pb.GeneratedFile
outPrefix := env.OutDir + "/"
for _, d := range deps {
if relPath, ok := strings.CutPrefix(d, outPrefix); ok {
contents, err := os.ReadFile(d)
if err != nil {
fmt.Printf("Generated file %q not found - will be skipped.\n", d)
continue
}
generated = append(generated, &pb.GeneratedFile{
Path: relPath,
Contents: contents,
})
}
}
moduleToDeps[m.Name] = &depsAndGenerated{deps, generated}
file.Generated = generated
file.Deps = deps
}
return &pb.IdeAnalysis{
Sources: sources,
}
}
// runMake runs Soong build for the given modules.
func runMake(ctx context.Context, env Env, modules ...string) error {
args := []string{
"--make-mode",
"ANDROID_BUILD_ENVIRONMENT_CONFIG=googler-cog",
"TARGET_PRODUCT=" + env.LunchTarget.Product,
"TARGET_RELEASE=" + env.LunchTarget.Release,
"TARGET_BUILD_VARIANT=" + env.LunchTarget.Variant,
"-k",
}
args = append(args, modules...)
cmd := exec.CommandContext(ctx, "build/soong/soong_ui.bash", args...)
cmd.Dir = env.RepoDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
type javaModule struct {
Name string
Path []string `json:"path,omitempty"`
Deps []string `json:"dependencies,omitempty"`
Srcs []string `json:"srcs,omitempty"`
Jars []string `json:"jars,omitempty"`
SrcJars []string `json:"srcjars,omitempty"`
}
func loadJavaModules(env *Env) (map[string]*javaModule, map[string]*javaModule, error) {
javaDepsPath := path.Join(env.RepoDir, env.OutDir, "soong/module_bp_java_deps.json")
data, err := os.ReadFile(javaDepsPath)
if err != nil {
return nil, nil, err
}
var moduleMapping map[string]*javaModule // module name -> module
if err = json.Unmarshal(data, &moduleMapping); err != nil {
return nil, nil, err
}
javaModules := make(map[string]*javaModule)
javaFileToModuleMap := make(map[string]*javaModule)
for name, module := range moduleMapping {
if strings.HasSuffix(name, "-jarjar") || strings.HasSuffix(name, ".impl") {
continue
}
module.Name = name
javaModules[name] = module
for _, src := range module.Srcs {
if !slices.Contains(env.JavaFiles, src) {
// We are only interested in active files.
continue
}
if javaFileToModuleMap[src] != nil {
// TODO(michaelmerg): Handle the case where a file is covered by multiple modules.
log.Printf("File %q found in module %q but is already covered by module %q", src, module.Name, javaFileToModuleMap[src].Name)
continue
}
javaFileToModuleMap[src] = module
}
}
return javaModules, javaFileToModuleMap, nil
}
func transitiveDeps(m *javaModule, modules map[string]*javaModule) []string {
var ret []string
q := list.New()
q.PushBack(m.Name)
seen := make(map[string]bool) // module names -> true
for q.Len() > 0 {
name := q.Remove(q.Front()).(string)
mod := modules[name]
if mod == nil {
continue
}
ret = append(ret, mod.Srcs...)
ret = append(ret, mod.SrcJars...)
ret = append(ret, mod.Jars...)
for _, d := range mod.Deps {
if seen[d] {
continue
}
seen[d] = true
q.PushBack(d)
}
}
slices.Sort(ret)
ret = slices.Compact(ret)
return ret
}