1/* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17// Binary ide_query generates and analyzes build artifacts. 18// The produced result can be consumed by IDEs to provide language features. 19package main 20 21import ( 22 "bytes" 23 "container/list" 24 "context" 25 "encoding/json" 26 "flag" 27 "fmt" 28 "log" 29 "os" 30 "os/exec" 31 "path" 32 "slices" 33 "strings" 34 35 "google.golang.org/protobuf/proto" 36 apb "ide_query/cc_analyzer_proto" 37 pb "ide_query/ide_query_proto" 38) 39 40// Env contains information about the current environment. 41type Env struct { 42 LunchTarget LunchTarget 43 RepoDir string 44 OutDir string 45 ClangToolsRoot string 46} 47 48// LunchTarget is a parsed Android lunch target. 49// Input format: <product_name>-<release_type>-<build_variant> 50type LunchTarget struct { 51 Product string 52 Release string 53 Variant string 54} 55 56var _ flag.Value = (*LunchTarget)(nil) 57 58// // Get implements flag.Value. 59// func (l *LunchTarget) Get() any { 60// return l 61// } 62 63// Set implements flag.Value. 64func (l *LunchTarget) Set(s string) error { 65 parts := strings.Split(s, "-") 66 if len(parts) != 3 { 67 return fmt.Errorf("invalid lunch target: %q, must have form <product_name>-<release_type>-<build_variant>", s) 68 } 69 *l = LunchTarget{ 70 Product: parts[0], 71 Release: parts[1], 72 Variant: parts[2], 73 } 74 return nil 75} 76 77// String implements flag.Value. 78func (l *LunchTarget) String() string { 79 return fmt.Sprintf("%s-%s-%s", l.Product, l.Release, l.Variant) 80} 81 82func main() { 83 var env Env 84 env.OutDir = strings.TrimSuffix(os.Getenv("OUT_DIR"), "/") 85 env.RepoDir = os.Getenv("ANDROID_BUILD_TOP") 86 env.ClangToolsRoot = os.Getenv("PREBUILTS_CLANG_TOOLS_ROOT") 87 flag.Var(&env.LunchTarget, "lunch_target", "The lunch target to query") 88 flag.Parse() 89 files := flag.Args() 90 if len(files) == 0 { 91 fmt.Println("No files provided.") 92 os.Exit(1) 93 return 94 } 95 96 var ccFiles, javaFiles []string 97 for _, f := range files { 98 switch { 99 case strings.HasSuffix(f, ".java") || strings.HasSuffix(f, ".kt"): 100 javaFiles = append(javaFiles, f) 101 case strings.HasSuffix(f, ".cc") || strings.HasSuffix(f, ".cpp") || strings.HasSuffix(f, ".h"): 102 ccFiles = append(ccFiles, f) 103 default: 104 log.Printf("File %q is supported - will be skipped.", f) 105 } 106 } 107 108 ctx := context.Background() 109 // TODO(michaelmerg): Figure out if module_bp_java_deps.json and compile_commands.json is outdated. 110 runMake(ctx, env, "nothing") 111 112 javaModules, err := loadJavaModules(env) 113 if err != nil { 114 log.Printf("Failed to load java modules: %v", err) 115 } 116 117 var targets []string 118 javaTargetsByFile := findJavaModules(javaFiles, javaModules) 119 for _, t := range javaTargetsByFile { 120 targets = append(targets, t) 121 } 122 123 ccTargets, err := getCCTargets(ctx, env, ccFiles) 124 if err != nil { 125 log.Fatalf("Failed to query cc targets: %v", err) 126 } 127 targets = append(targets, ccTargets...) 128 if len(targets) == 0 { 129 fmt.Println("No targets found.") 130 os.Exit(1) 131 return 132 } 133 134 fmt.Fprintf(os.Stderr, "Running make for modules: %v\n", strings.Join(targets, ", ")) 135 if err := runMake(ctx, env, targets...); err != nil { 136 log.Printf("Building modules failed: %v", err) 137 } 138 139 var analysis pb.IdeAnalysis 140 results, units := getJavaInputs(env, javaTargetsByFile, javaModules) 141 analysis.Results = results 142 analysis.Units = units 143 if err != nil && analysis.Error == nil { 144 analysis.Error = &pb.AnalysisError{ 145 ErrorMessage: err.Error(), 146 } 147 } 148 149 results, units, err = getCCInputs(ctx, env, ccFiles) 150 analysis.Results = append(analysis.Results, results...) 151 analysis.Units = append(analysis.Units, units...) 152 if err != nil && analysis.Error == nil { 153 analysis.Error = &pb.AnalysisError{ 154 ErrorMessage: err.Error(), 155 } 156 } 157 158 analysis.BuildOutDir = env.OutDir 159 data, err := proto.Marshal(&analysis) 160 if err != nil { 161 log.Fatalf("Failed to marshal result proto: %v", err) 162 } 163 164 _, err = os.Stdout.Write(data) 165 if err != nil { 166 log.Fatalf("Failed to write result proto: %v", err) 167 } 168 169 for _, r := range analysis.Results { 170 fmt.Fprintf(os.Stderr, "%s: %+v\n", r.GetSourceFilePath(), r.GetStatus()) 171 } 172} 173 174func repoState(env Env, filePaths []string) *apb.RepoState { 175 const compDbPath = "soong/development/ide/compdb/compile_commands.json" 176 return &apb.RepoState{ 177 RepoDir: env.RepoDir, 178 ActiveFilePath: filePaths, 179 OutDir: env.OutDir, 180 CompDbPath: path.Join(env.OutDir, compDbPath), 181 } 182} 183 184func runCCanalyzer(ctx context.Context, env Env, mode string, in []byte) ([]byte, error) { 185 ccAnalyzerPath := path.Join(env.ClangToolsRoot, "bin/ide_query_cc_analyzer") 186 outBuffer := new(bytes.Buffer) 187 188 inBuffer := new(bytes.Buffer) 189 inBuffer.Write(in) 190 191 cmd := exec.CommandContext(ctx, ccAnalyzerPath, "--mode="+mode) 192 cmd.Dir = env.RepoDir 193 194 cmd.Stdin = inBuffer 195 cmd.Stdout = outBuffer 196 cmd.Stderr = os.Stderr 197 198 err := cmd.Run() 199 200 return outBuffer.Bytes(), err 201} 202 203// Execute cc_analyzer and get all the targets that needs to be build for analyzing files. 204func getCCTargets(ctx context.Context, env Env, filePaths []string) ([]string, error) { 205 state, err := proto.Marshal(repoState(env, filePaths)) 206 if err != nil { 207 log.Fatalln("Failed to serialize state:", err) 208 } 209 210 resp := new(apb.DepsResponse) 211 result, err := runCCanalyzer(ctx, env, "deps", state) 212 if err != nil { 213 return nil, err 214 } 215 216 if err := proto.Unmarshal(result, resp); err != nil { 217 return nil, fmt.Errorf("malformed response from cc_analyzer: %v", err) 218 } 219 220 var targets []string 221 if resp.Status != nil && resp.Status.Code != apb.Status_OK { 222 return targets, fmt.Errorf("cc_analyzer failed: %v", resp.Status.Message) 223 } 224 225 for _, deps := range resp.Deps { 226 targets = append(targets, deps.BuildTarget...) 227 } 228 return targets, nil 229} 230 231func getCCInputs(ctx context.Context, env Env, filePaths []string) ([]*pb.AnalysisResult, []*pb.BuildableUnit, error) { 232 state, err := proto.Marshal(repoState(env, filePaths)) 233 if err != nil { 234 log.Fatalln("Failed to serialize state:", err) 235 } 236 237 resp := new(apb.IdeAnalysis) 238 result, err := runCCanalyzer(ctx, env, "inputs", state) 239 if err != nil { 240 return nil, nil, fmt.Errorf("cc_analyzer failed:", err) 241 } 242 if err := proto.Unmarshal(result, resp); err != nil { 243 return nil, nil, fmt.Errorf("malformed response from cc_analyzer: %v", err) 244 } 245 if resp.Status != nil && resp.Status.Code != apb.Status_OK { 246 return nil, nil, fmt.Errorf("cc_analyzer failed: %v", resp.Status.Message) 247 } 248 249 var results []*pb.AnalysisResult 250 var units []*pb.BuildableUnit 251 for _, s := range resp.Sources { 252 status := &pb.AnalysisResult_Status{ 253 Code: pb.AnalysisResult_Status_CODE_OK, 254 } 255 if s.GetStatus().GetCode() != apb.Status_OK { 256 status.Code = pb.AnalysisResult_Status_CODE_BUILD_FAILED 257 status.StatusMessage = proto.String(s.GetStatus().GetMessage()) 258 } 259 260 result := &pb.AnalysisResult{ 261 SourceFilePath: s.GetPath(), 262 UnitId: s.GetPath(), 263 Status: status, 264 } 265 results = append(results, result) 266 267 var generated []*pb.GeneratedFile 268 for _, f := range s.Generated { 269 generated = append(generated, &pb.GeneratedFile{ 270 Path: f.GetPath(), 271 Contents: f.GetContents(), 272 }) 273 } 274 genUnit := &pb.BuildableUnit{ 275 Id: "genfiles_for_" + s.GetPath(), 276 SourceFilePaths: s.GetDeps(), 277 GeneratedFiles: generated, 278 } 279 280 unit := &pb.BuildableUnit{ 281 Id: s.GetPath(), 282 Language: pb.Language_LANGUAGE_CPP, 283 SourceFilePaths: []string{s.GetPath()}, 284 CompilerArguments: s.GetCompilerArguments(), 285 DependencyIds: []string{genUnit.GetId()}, 286 } 287 units = append(units, unit, genUnit) 288 } 289 return results, units, nil 290} 291 292// findJavaModules tries to find the modules that cover the given file paths. 293// If a file is covered by multiple modules, the first module is returned. 294func findJavaModules(paths []string, modules map[string]*javaModule) map[string]string { 295 ret := make(map[string]string) 296 // A file may be part of multiple modules. To make the result deterministic, 297 // check the modules in sorted order. 298 keys := make([]string, 0, len(modules)) 299 for name := range modules { 300 keys = append(keys, name) 301 } 302 slices.Sort(keys) 303 for _, name := range keys { 304 if strings.HasSuffix(name, ".impl") { 305 continue 306 } 307 308 module := modules[name] 309 for i, p := range paths { 310 if slices.Contains(module.Srcs, p) { 311 ret[p] = name 312 paths = append(paths[:i], paths[i+1:]...) 313 break 314 } 315 } 316 if len(paths) == 0 { 317 break 318 } 319 } 320 return ret 321} 322 323func getJavaInputs(env Env, modulesByPath map[string]string, modules map[string]*javaModule) ([]*pb.AnalysisResult, []*pb.BuildableUnit) { 324 var results []*pb.AnalysisResult 325 unitsById := make(map[string]*pb.BuildableUnit) 326 for p, moduleName := range modulesByPath { 327 r := &pb.AnalysisResult{ 328 SourceFilePath: p, 329 } 330 results = append(results, r) 331 332 m := modules[moduleName] 333 if m == nil { 334 r.Status = &pb.AnalysisResult_Status{ 335 Code: pb.AnalysisResult_Status_CODE_NOT_FOUND, 336 StatusMessage: proto.String("File not found in any module."), 337 } 338 continue 339 } 340 341 r.UnitId = moduleName 342 r.Status = &pb.AnalysisResult_Status{Code: pb.AnalysisResult_Status_CODE_OK} 343 if unitsById[r.UnitId] != nil { 344 // File is covered by an already created unit. 345 continue 346 } 347 348 u := &pb.BuildableUnit{ 349 Id: moduleName, 350 Language: pb.Language_LANGUAGE_JAVA, 351 SourceFilePaths: m.Srcs, 352 GeneratedFiles: genFiles(env, m), 353 DependencyIds: m.Deps, 354 } 355 unitsById[u.Id] = u 356 357 q := list.New() 358 for _, d := range m.Deps { 359 q.PushBack(d) 360 } 361 for q.Len() > 0 { 362 name := q.Remove(q.Front()).(string) 363 mod := modules[name] 364 if mod == nil || unitsById[name] != nil { 365 continue 366 } 367 368 unitsById[name] = &pb.BuildableUnit{ 369 Id: name, 370 SourceFilePaths: mod.Srcs, 371 GeneratedFiles: genFiles(env, mod), 372 DependencyIds: mod.Deps, 373 } 374 375 for _, d := range mod.Deps { 376 q.PushBack(d) 377 } 378 } 379 } 380 381 units := make([]*pb.BuildableUnit, 0, len(unitsById)) 382 for _, u := range unitsById { 383 units = append(units, u) 384 } 385 return results, units 386} 387 388// genFiles returns the generated files (paths that start with outDir/) for the 389// given module. Generated files that do not exist are ignored. 390func genFiles(env Env, mod *javaModule) []*pb.GeneratedFile { 391 var paths []string 392 paths = append(paths, mod.Srcs...) 393 paths = append(paths, mod.SrcJars...) 394 paths = append(paths, mod.Jars...) 395 396 prefix := env.OutDir + "/" 397 var ret []*pb.GeneratedFile 398 for _, p := range paths { 399 relPath, ok := strings.CutPrefix(p, prefix) 400 if !ok { 401 continue 402 } 403 404 contents, err := os.ReadFile(path.Join(env.RepoDir, p)) 405 if err != nil { 406 continue 407 } 408 409 ret = append(ret, &pb.GeneratedFile{ 410 Path: relPath, 411 Contents: contents, 412 }) 413 } 414 return ret 415} 416 417// runMake runs Soong build for the given modules. 418func runMake(ctx context.Context, env Env, modules ...string) error { 419 args := []string{ 420 "--make-mode", 421 "ANDROID_BUILD_ENVIRONMENT_CONFIG=googler-cog", 422 "SOONG_GEN_COMPDB=1", 423 "TARGET_PRODUCT=" + env.LunchTarget.Product, 424 "TARGET_RELEASE=" + env.LunchTarget.Release, 425 "TARGET_BUILD_VARIANT=" + env.LunchTarget.Variant, 426 "TARGET_BUILD_TYPE=release", 427 "-k", 428 } 429 args = append(args, modules...) 430 cmd := exec.CommandContext(ctx, "build/soong/soong_ui.bash", args...) 431 cmd.Dir = env.RepoDir 432 cmd.Stdout = os.Stderr 433 cmd.Stderr = os.Stderr 434 return cmd.Run() 435} 436 437type javaModule struct { 438 Path []string `json:"path,omitempty"` 439 Deps []string `json:"dependencies,omitempty"` 440 Srcs []string `json:"srcs,omitempty"` 441 Jars []string `json:"jars,omitempty"` 442 SrcJars []string `json:"srcjars,omitempty"` 443} 444 445func loadJavaModules(env Env) (map[string]*javaModule, error) { 446 javaDepsPath := path.Join(env.RepoDir, env.OutDir, "soong/module_bp_java_deps.json") 447 data, err := os.ReadFile(javaDepsPath) 448 if err != nil { 449 return nil, err 450 } 451 452 var ret map[string]*javaModule // module name -> module 453 if err = json.Unmarshal(data, &ret); err != nil { 454 return nil, err 455 } 456 457 // Add top level java_sdk_library for .impl modules. 458 for name, module := range ret { 459 if striped := strings.TrimSuffix(name, ".impl"); striped != name { 460 ret[striped] = module 461 } 462 } 463 return ret, nil 464} 465