blob: c1c4da0ed5d47070f9d58b59436d110f590d68c5 [file] [log] [blame]
Michael Merg6bafd752024-02-12 13:52:00 +00001// Binary ide_query generates and analyzes build artifacts.
2// The produced result can be consumed by IDEs to provide language features.
3package main
4
5import (
6 "container/list"
7 "context"
8 "encoding/json"
9 "flag"
10 "fmt"
11 "log"
12 "os"
13 "os/exec"
14 "path"
15 "slices"
16 "strings"
17
18 "google.golang.org/protobuf/proto"
19 pb "ide_query/ide_query_proto"
20)
21
22// Env contains information about the current environment.
23type Env struct {
24 LunchTarget LunchTarget
25 RepoDir string
26 OutDir string
27}
28
29// LunchTarget is a parsed Android lunch target.
30// Input format: <product_name>-<release_type>-<build_variant>
31type LunchTarget struct {
32 Product string
33 Release string
34 Variant string
35}
36
37var _ flag.Value = (*LunchTarget)(nil)
38
39// // Get implements flag.Value.
40// func (l *LunchTarget) Get() any {
41// return l
42// }
43
44// Set implements flag.Value.
45func (l *LunchTarget) Set(s string) error {
46 parts := strings.Split(s, "-")
47 if len(parts) != 3 {
48 return fmt.Errorf("invalid lunch target: %q, must have form <product_name>-<release_type>-<build_variant>", s)
49 }
50 *l = LunchTarget{
51 Product: parts[0],
52 Release: parts[1],
53 Variant: parts[2],
54 }
55 return nil
56}
57
58// String implements flag.Value.
59func (l *LunchTarget) String() string {
60 return fmt.Sprintf("%s-%s-%s", l.Product, l.Release, l.Variant)
61}
62
63func main() {
64 var env Env
65 env.OutDir = os.Getenv("OUT_DIR")
66 env.RepoDir = os.Getenv("ANDROID_BUILD_TOP")
67 flag.Var(&env.LunchTarget, "lunch_target", "The lunch target to query")
68 flag.Parse()
69 files := flag.Args()
70 if len(files) == 0 {
71 fmt.Println("No files provided.")
72 os.Exit(1)
73 return
74 }
75
76 var javaFiles []string
77 for _, f := range files {
78 switch {
79 case strings.HasSuffix(f, ".java") || strings.HasSuffix(f, ".kt"):
80 javaFiles = append(javaFiles, f)
81 default:
82 log.Printf("File %q is supported - will be skipped.", f)
83 }
84 }
85
86 ctx := context.Background()
87 javaDepsPath := path.Join(env.RepoDir, env.OutDir, "soong/module_bp_java_deps.json")
88 // TODO(michaelmerg): Figure out if module_bp_java_deps.json is outdated.
89 runMake(ctx, env, "nothing")
90
91 javaModules, err := loadJavaModules(javaDepsPath)
92 if err != nil {
93 log.Fatalf("Failed to load java modules: %v", err)
94 }
95
96 fileToModule := make(map[string]*javaModule) // file path -> module
97 for _, f := range javaFiles {
98 for _, m := range javaModules {
99 if !slices.Contains(m.Srcs, f) {
100 continue
101 }
102 if fileToModule[f] != nil {
103 // TODO(michaelmerg): Handle the case where a file is covered by multiple modules.
104 log.Printf("File %q found in module %q but is already covered by module %q", f, m.Name, fileToModule[f].Name)
105 continue
106 }
107 fileToModule[f] = m
108 }
109 }
110
111 var toMake []string
112 for _, m := range fileToModule {
113 toMake = append(toMake, m.Name)
114 }
115 fmt.Printf("Running make for modules: %v\n", strings.Join(toMake, ", "))
116 if err := runMake(ctx, env, toMake...); err != nil {
117 log.Fatalf("Failed to run make: %v", err)
118 }
119
120 var sources []*pb.SourceFile
121 type depsAndGenerated struct {
122 Deps []string
123 Generated []*pb.GeneratedFile
124 }
125 moduleToDeps := make(map[string]*depsAndGenerated)
126 for _, f := range files {
127 file := &pb.SourceFile{
128 Path: f,
129 WorkingDir: env.RepoDir,
130 }
131 sources = append(sources, file)
132
133 m := fileToModule[f]
134 if m == nil {
135 file.Status = &pb.Status{
136 Code: pb.Status_FAILURE,
137 Message: proto.String("File not found in any module."),
138 }
139 continue
140 }
141
142 file.Status = &pb.Status{Code: pb.Status_OK}
143 if moduleToDeps[m.Name] != nil {
144 file.Generated = moduleToDeps[m.Name].Generated
145 file.Deps = moduleToDeps[m.Name].Deps
146 continue
147 }
148
149 deps := transitiveDeps(m, javaModules)
150 var generated []*pb.GeneratedFile
151 outPrefix := env.OutDir + "/"
152 for _, d := range deps {
153 if relPath, ok := strings.CutPrefix(d, outPrefix); ok {
154 contents, err := os.ReadFile(d)
155 if err != nil {
156 fmt.Printf("Generated file %q not found - will be skipped.\n", d)
157 continue
158 }
159
160 generated = append(generated, &pb.GeneratedFile{
161 Path: relPath,
162 Contents: contents,
163 })
164 }
165 }
166 moduleToDeps[m.Name] = &depsAndGenerated{deps, generated}
167 file.Generated = generated
168 file.Deps = deps
169 }
170
171 res := &pb.IdeAnalysis{
172 BuildArtifactRoot: env.OutDir,
173 Sources: sources,
174 Status: &pb.Status{Code: pb.Status_OK},
175 }
176 data, err := proto.Marshal(res)
177 if err != nil {
178 log.Fatalf("Failed to marshal result proto: %v", err)
179 }
180
181 err = os.WriteFile(path.Join(env.OutDir, "ide_query.pb"), data, 0644)
182 if err != nil {
183 log.Fatalf("Failed to write result proto: %v", err)
184 }
185
186 for _, s := range sources {
187 fmt.Printf("%s: %v (Deps: %d, Generated: %d)\n", s.GetPath(), s.GetStatus(), len(s.GetDeps()), len(s.GetGenerated()))
188 }
189}
190
191// runMake runs Soong build for the given modules.
192func runMake(ctx context.Context, env Env, modules ...string) error {
193 args := []string{
194 "--make-mode",
195 "ANDROID_BUILD_ENVIRONMENT_CONFIG=googler-cog",
196 "TARGET_PRODUCT=" + env.LunchTarget.Product,
197 "TARGET_RELEASE=" + env.LunchTarget.Release,
198 "TARGET_BUILD_VARIANT=" + env.LunchTarget.Variant,
199 }
200 args = append(args, modules...)
201 cmd := exec.CommandContext(ctx, "build/soong/soong_ui.bash", args...)
202 cmd.Dir = env.RepoDir
203 cmd.Stdout = os.Stdout
204 cmd.Stderr = os.Stderr
205 return cmd.Run()
206}
207
208type javaModule struct {
209 Name string
210 Path []string `json:"path,omitempty"`
211 Deps []string `json:"dependencies,omitempty"`
212 Srcs []string `json:"srcs,omitempty"`
213 Jars []string `json:"jars,omitempty"`
214 SrcJars []string `json:"srcjars,omitempty"`
215}
216
217func loadJavaModules(path string) (map[string]*javaModule, error) {
218 data, err := os.ReadFile(path)
219 if err != nil {
220 return nil, err
221 }
222
223 var ret map[string]*javaModule // module name -> module
224 if err = json.Unmarshal(data, &ret); err != nil {
225 return nil, err
226 }
227
228 for name, module := range ret {
229 if strings.HasSuffix(name, "-jarjar") || strings.HasSuffix(name, ".impl") {
230 delete(ret, name)
231 continue
232 }
233
234 module.Name = name
235 }
236 return ret, nil
237}
238
239func transitiveDeps(m *javaModule, modules map[string]*javaModule) []string {
240 var ret []string
241 q := list.New()
242 q.PushBack(m.Name)
243 seen := make(map[string]bool) // module names -> true
244 for q.Len() > 0 {
245 name := q.Remove(q.Front()).(string)
246 mod := modules[name]
247 if mod == nil {
248 continue
249 }
250
251 ret = append(ret, mod.Srcs...)
252 ret = append(ret, mod.SrcJars...)
253 ret = append(ret, mod.Jars...)
254 for _, d := range mod.Deps {
255 if seen[d] {
256 continue
257 }
258 seen[d] = true
259 q.PushBack(d)
260 }
261 }
262 slices.Sort(ret)
263 ret = slices.Compact(ret)
264 return ret
265}