• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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