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 _, target := range javaTargetsByFile { 120 targets = append(targets, javaModules[target].Jars...) 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 if len(module.Jars) == 0 { 310 continue 311 } 312 313 for i, p := range paths { 314 if slices.Contains(module.Srcs, p) { 315 ret[p] = name 316 paths = append(paths[:i], paths[i+1:]...) 317 break 318 } 319 } 320 if len(paths) == 0 { 321 break 322 } 323 } 324 325 return ret 326} 327 328func getJavaInputs(env Env, modulesByPath map[string]string, modules map[string]*javaModule) ([]*pb.AnalysisResult, []*pb.BuildableUnit) { 329 var results []*pb.AnalysisResult 330 unitsById := make(map[string]*pb.BuildableUnit) 331 for p, moduleName := range modulesByPath { 332 r := &pb.AnalysisResult{ 333 SourceFilePath: p, 334 } 335 results = append(results, r) 336 337 m := modules[moduleName] 338 if m == nil { 339 r.Status = &pb.AnalysisResult_Status{ 340 Code: pb.AnalysisResult_Status_CODE_NOT_FOUND, 341 StatusMessage: proto.String("File not found in any module."), 342 } 343 continue 344 } 345 346 r.UnitId = moduleName 347 r.Status = &pb.AnalysisResult_Status{Code: pb.AnalysisResult_Status_CODE_OK} 348 if unitsById[r.UnitId] != nil { 349 // File is covered by an already created unit. 350 continue 351 } 352 353 u := &pb.BuildableUnit{ 354 Id: moduleName, 355 Language: pb.Language_LANGUAGE_JAVA, 356 SourceFilePaths: m.Srcs, 357 GeneratedFiles: genFiles(env, m), 358 DependencyIds: m.Deps, 359 } 360 unitsById[u.Id] = u 361 362 q := list.New() 363 for _, d := range m.Deps { 364 q.PushBack(d) 365 } 366 for q.Len() > 0 { 367 name := q.Remove(q.Front()).(string) 368 mod := modules[name] 369 if mod == nil || unitsById[name] != nil { 370 continue 371 } 372 373 unitsById[name] = &pb.BuildableUnit{ 374 Id: name, 375 SourceFilePaths: mod.Srcs, 376 GeneratedFiles: genFiles(env, mod), 377 DependencyIds: mod.Deps, 378 } 379 380 for _, d := range mod.Deps { 381 q.PushBack(d) 382 } 383 } 384 } 385 386 units := make([]*pb.BuildableUnit, 0, len(unitsById)) 387 for _, u := range unitsById { 388 units = append(units, u) 389 } 390 return results, units 391} 392 393// genFiles returns the generated files (paths that start with outDir/) for the 394// given module. Generated files that do not exist are ignored. 395func genFiles(env Env, mod *javaModule) []*pb.GeneratedFile { 396 var paths []string 397 paths = append(paths, mod.Srcs...) 398 paths = append(paths, mod.SrcJars...) 399 paths = append(paths, mod.Jars...) 400 401 prefix := env.OutDir + "/" 402 var ret []*pb.GeneratedFile 403 for _, p := range paths { 404 relPath, ok := strings.CutPrefix(p, prefix) 405 if !ok { 406 continue 407 } 408 409 contents, err := os.ReadFile(path.Join(env.RepoDir, p)) 410 if err != nil { 411 continue 412 } 413 414 ret = append(ret, &pb.GeneratedFile{ 415 Path: relPath, 416 Contents: contents, 417 }) 418 } 419 return ret 420} 421 422// runMake runs Soong build for the given modules. 423func runMake(ctx context.Context, env Env, modules ...string) error { 424 args := []string{ 425 "--make-mode", 426 "ANDROID_BUILD_ENVIRONMENT_CONFIG=googler-cog", 427 "SOONG_GEN_COMPDB=1", 428 "TARGET_PRODUCT=" + env.LunchTarget.Product, 429 "TARGET_RELEASE=" + env.LunchTarget.Release, 430 "TARGET_BUILD_VARIANT=" + env.LunchTarget.Variant, 431 "TARGET_BUILD_TYPE=release", 432 "-k", 433 } 434 args = append(args, modules...) 435 cmd := exec.CommandContext(ctx, "build/soong/soong_ui.bash", args...) 436 cmd.Dir = env.RepoDir 437 cmd.Stdout = os.Stderr 438 cmd.Stderr = os.Stderr 439 return cmd.Run() 440} 441 442type javaModule struct { 443 Path []string `json:"path,omitempty"` 444 Deps []string `json:"dependencies,omitempty"` 445 Srcs []string `json:"srcs,omitempty"` 446 Jars []string `json:"jars,omitempty"` 447 SrcJars []string `json:"srcjars,omitempty"` 448} 449 450func loadJavaModules(env Env) (map[string]*javaModule, error) { 451 javaDepsPath := path.Join(env.RepoDir, env.OutDir, "soong/module_bp_java_deps.json") 452 data, err := os.ReadFile(javaDepsPath) 453 if err != nil { 454 return nil, err 455 } 456 457 var ret map[string]*javaModule // module name -> module 458 if err = json.Unmarshal(data, &ret); err != nil { 459 return nil, err 460 } 461 462 // Add top level java_sdk_library for .impl modules. 463 for name, module := range ret { 464 if striped := strings.TrimSuffix(name, ".impl"); striped != name { 465 ret[striped] = module 466 } 467 } 468 return ret, nil 469} 470