Add CC analysis support to ide_query
Introduces ide_query_cc_analyzer, which figures out relevant build targets that needs to be built for a given C++ source or header file.
Once these targets are built, it analyzes the sources in question and reports any generated files that are used back.
Full ide_query integration relies on this binary also being available in prebuilts clang-tools, it'll be done in a future patch.
Change-Id: Ib0ef6da7a2bc8ecf66940b326e037fb1ee230bf9
diff --git a/tools/ide_query/ide_query.go b/tools/ide_query/ide_query.go
index c1c4da0..9645a02 100644
--- a/tools/ide_query/ide_query.go
+++ b/tools/ide_query/ide_query.go
@@ -1,8 +1,25 @@
+/*
+ * 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"
@@ -21,9 +38,13 @@
// Env contains information about the current environment.
type Env struct {
- LunchTarget LunchTarget
- RepoDir string
- OutDir string
+ LunchTarget LunchTarget
+ RepoDir string
+ OutDir string
+ ClangToolsRoot string
+
+ CcFiles []string
+ JavaFiles []string
}
// LunchTarget is a parsed Android lunch target.
@@ -64,6 +85,7 @@
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()
@@ -73,64 +95,169 @@
return
}
- var javaFiles []string
for _, f := range files {
switch {
case strings.HasSuffix(f, ".java") || strings.HasSuffix(f, ".kt"):
- javaFiles = append(javaFiles, f)
+ 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()
- javaDepsPath := path.Join(env.RepoDir, env.OutDir, "soong/module_bp_java_deps.json")
- // TODO(michaelmerg): Figure out if module_bp_java_deps.json is outdated.
+ // TODO(michaelmerg): Figure out if module_bp_java_deps.json and compile_commands.json is outdated.
runMake(ctx, env, "nothing")
- javaModules, err := loadJavaModules(javaDepsPath)
+ javaModules, javaFileToModuleMap, err := loadJavaModules(&env)
if err != nil {
- log.Fatalf("Failed to load java modules: %v", err)
+ 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)
}
- fileToModule := make(map[string]*javaModule) // file path -> module
- for _, f := range javaFiles {
- for _, m := range javaModules {
- if !slices.Contains(m.Srcs, f) {
- continue
- }
- if fileToModule[f] != 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", f, m.Name, fileToModule[f].Name)
- continue
- }
- fileToModule[f] = m
+ 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 toMake []string
- for _, m := range fileToModule {
- toMake = append(toMake, m.Name)
+ var targets []string
+ if resp.Status != nil && resp.Status.Code != pb.Status_OK {
+ return targets, resp.Status
}
- fmt.Printf("Running make for modules: %v\n", strings.Join(toMake, ", "))
- if err := runMake(ctx, env, toMake...); err != nil {
- log.Fatalf("Failed to run make: %v", err)
+ 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 files {
+ for _, f := range env.JavaFiles {
file := &pb.SourceFile{
- Path: f,
- WorkingDir: env.RepoDir,
+ Path: f,
}
sources = append(sources, file)
- m := fileToModule[f]
+ m := javaFileToModuleMap[f]
if m == nil {
file.Status = &pb.Status{
Code: pb.Status_FAILURE,
@@ -167,24 +294,8 @@
file.Generated = generated
file.Deps = deps
}
-
- res := &pb.IdeAnalysis{
- BuildArtifactRoot: env.OutDir,
- Sources: sources,
- Status: &pb.Status{Code: pb.Status_OK},
- }
- data, err := proto.Marshal(res)
- if err != nil {
- log.Fatalf("Failed to marshal result proto: %v", err)
- }
-
- err = os.WriteFile(path.Join(env.OutDir, "ide_query.pb"), data, 0644)
- if err != nil {
- log.Fatalf("Failed to write result proto: %v", err)
- }
-
- for _, s := range sources {
- fmt.Printf("%s: %v (Deps: %d, Generated: %d)\n", s.GetPath(), s.GetStatus(), len(s.GetDeps()), len(s.GetGenerated()))
+ return &pb.IdeAnalysis{
+ Sources: sources,
}
}
@@ -214,26 +325,40 @@
SrcJars []string `json:"srcjars,omitempty"`
}
-func loadJavaModules(path string) (map[string]*javaModule, error) {
- data, err := os.ReadFile(path)
+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, err
+ return nil, nil, err
}
- var ret map[string]*javaModule // module name -> module
- if err = json.Unmarshal(data, &ret); err != nil {
- return nil, err
+ var moduleMapping map[string]*javaModule // module name -> module
+ if err = json.Unmarshal(data, &moduleMapping); err != nil {
+ return nil, nil, err
}
- for name, module := range ret {
+ javaModules := make(map[string]*javaModule)
+ javaFileToModuleMap := make(map[string]*javaModule)
+ for name, module := range moduleMapping {
if strings.HasSuffix(name, "-jarjar") || strings.HasSuffix(name, ".impl") {
- delete(ret, name)
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 ret, nil
+ return javaModules, javaFileToModuleMap, nil
}
func transitiveDeps(m *javaModule, modules map[string]*javaModule) []string {