xref: /aosp_15_r20/build/make/tools/ide_query/ide_query.go (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
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