1// Copyright 2022 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package main 16 17import ( 18 "bufio" 19 "context" 20 "encoding/json" 21 "errors" 22 "flag" 23 "fmt" 24 "io" 25 "log" 26 "os" 27 "runtime" 28 "strings" 29 "time" 30 31 "tools/treble/build/report/app" 32 "tools/treble/build/report/local" 33 "tools/treble/build/report/report" 34) 35 36type Build interface { 37 Build(ctx context.Context, target string) *app.BuildCmdResult 38} 39 40type tool interface { 41 Run(ctx context.Context, rtx *report.Context, rsp *response) error 42 PrintText(w io.Writer, rsp *response, verbose bool) 43} 44type repoFlags []app.ProjectCommit 45 46func (r *repoFlags) Set(value string) error { 47 commit := app.ProjectCommit{} 48 items := strings.Split(value, ":") 49 if len(items) > 2 { 50 return (errors.New("Invalid repo value expected (proj:sha) format")) 51 } 52 commit.Project = items[0] 53 if len(items) > 1 { 54 commit.Revision = items[1] 55 } 56 *r = append(*r, commit) 57 return nil 58} 59func (r *repoFlags) String() string { 60 items := []string{} 61 for _, fl := range *r { 62 items = append(items, fmt.Sprintf("%s:%s", fl.Project, fl.Revision)) 63 } 64 return strings.Join(items, " ") 65} 66 67var ( 68 // Common flags 69 ninjaDbPtr = flag.String("ninja", local.DefNinjaDb(), "Set the .ninja file to use when building metrics") 70 ninjaExcPtr = flag.String("ninja_cmd", local.DefNinjaExc(), "Set the ninja executable") 71 ninjaTimeoutStr = flag.String("ninja_timeout", local.DefaultNinjaTimeout, "Default ninja timeout") 72 buildTimeoutStr = flag.String("build_timeout", local.DefaultNinjaBuildTimeout, "Default build timeout") 73 manifestPtr = flag.String("manifest", local.DefManifest(), "Set the location of the manifest file") 74 upstreamPtr = flag.String("upstream", "", "Upstream branch to compare files against") 75 repoBasePtr = flag.String("repo_base", local.DefRepoBase(), "Set the repo base directory") 76 workerCountPtr = flag.Int("worker_count", runtime.NumCPU(), "Number of worker routines") 77 buildWorkerCountPtr = flag.Int("build_worker_count", local.MaxNinjaCliWorkers, "Number of build worker routines") 78 clientServerPtr = flag.Bool("client_server", false, "Run client server mode") 79 buildPtr = flag.Bool("build", false, "Build targets") 80 jsonPtr = flag.Bool("json", false, "Print json data") 81 verbosePtr = flag.Bool("v", false, "Print verbose text data") 82 outputPtr = flag.String("o", "", "Output to file") 83 projsPtr = flag.Bool("projects", false, "Include project repo data") 84 85 hostFlags = flag.NewFlagSet("host", flag.ExitOnError) 86 queryFlags = flag.NewFlagSet("query", flag.ExitOnError) 87 pathsFlags = flag.NewFlagSet("paths", flag.ExitOnError) 88) 89 90// Add profiling data 91type profTime struct { 92 Description string `json:"description"` 93 DurationSecs float64 `json:"duration"` 94} 95 96type commit struct { 97 Project app.ProjectCommit `json:"project"` 98 Commit *app.GitCommit `json:"commit"` 99} 100 101// Use one structure for output for now 102type response struct { 103 Commits []commit `json:"commits,omitempty"` 104 Inputs []string `json:"files,omitempty"` 105 BuildFiles []*app.BuildCmdResult `json:"build_files,omitempty"` 106 Targets []string `json:"targets,omitempty"` 107 Report *app.Report `json:"report,omitempty"` 108 109 // Subcommand data 110 Query *app.QueryResponse `json:"query,omitempty"` 111 Paths []*app.BuildPath `json:"build_paths,omitempty"` 112 Host *app.HostReport `json:"host,omitempty"` 113 Projects map[string]*app.GitProject `json:"projects,omitempty"` 114 // Profile data 115 Profile []*profTime `json:"profile"` 116} 117 118func main() { 119 startTime := time.Now() 120 ctx := context.Background() 121 rsp := &response{} 122 123 var addProfileData = func(desc string) { 124 rsp.Profile = append(rsp.Profile, &profTime{Description: desc, DurationSecs: time.Since(startTime).Seconds()}) 125 startTime = time.Now() 126 } 127 flag.Parse() 128 129 ninjaTimeout, err := time.ParseDuration(*ninjaTimeoutStr) 130 if err != nil { 131 log.Fatalf("Invalid ninja timeout %s", *ninjaTimeoutStr) 132 } 133 134 buildTimeout, err := time.ParseDuration(*buildTimeoutStr) 135 if err != nil { 136 log.Fatalf("Invalid build timeout %s", *buildTimeoutStr) 137 } 138 139 subArgs := flag.Args() 140 defBuildTarget := "droid" 141 log.SetFlags(log.LstdFlags | log.Llongfile) 142 143 ninja := local.NewNinjaCli(*ninjaExcPtr, *ninjaDbPtr, ninjaTimeout, buildTimeout, *clientServerPtr) 144 145 if *clientServerPtr { 146 ninjaServ := local.NewNinjaServer(*ninjaExcPtr, *ninjaDbPtr) 147 defer ninjaServ.Kill() 148 go func() { 149 150 ninjaServ.Start(ctx) 151 }() 152 if err := ninja.WaitForServer(ctx, int(ninjaTimeout.Seconds())); err != nil { 153 log.Fatalf("Failed to connect to server") 154 } 155 } 156 rtx := &report.Context{ 157 RepoBase: *repoBasePtr, 158 Repo: &report.RepoMan{}, 159 Build: ninja, 160 Project: local.NewGitCli(), 161 WorkerCount: *workerCountPtr, 162 BuildWorkerCount: *buildWorkerCountPtr, 163 } 164 165 var subcommand tool 166 var commits repoFlags 167 if len(subArgs) > 0 { 168 switch subArgs[0] { 169 case "host": 170 hostToolPathPtr := hostFlags.String("hostbin", local.DefHostBinPath(), "Set the output directory for host tools") 171 hostFlags.Parse(subArgs[1:]) 172 173 subcommand = &hostReport{toolPath: *hostToolPathPtr} 174 rsp.Targets = hostFlags.Args() 175 176 case "query": 177 queryFlags.Var(&commits, "repo", "Repo:SHA to query") 178 queryFlags.Parse(subArgs[1:]) 179 subcommand = &queryReport{} 180 rsp.Targets = queryFlags.Args() 181 182 case "paths": 183 pathsFlags.Var(&commits, "repo", "Repo:SHA to build") 184 singlePathPtr := pathsFlags.Bool("1", false, "Get single path to output target") 185 pathsFlags.Parse(subArgs[1:]) 186 187 subcommand = &pathsReport{build_target: defBuildTarget, single: *singlePathPtr} 188 189 rsp.Inputs = pathsFlags.Args() 190 191 default: 192 rsp.Targets = subArgs 193 } 194 } 195 addProfileData("Init") 196 rtx.ResolveProjectMap(ctx, *manifestPtr, *upstreamPtr) 197 addProfileData("Project Map") 198 199 // Add project to output if requested 200 if *projsPtr == true { 201 rsp.Projects = make(map[string]*app.GitProject) 202 for k, p := range rtx.Info.ProjMap { 203 rsp.Projects[k] = p.GitProj 204 } 205 } 206 207 // Resolve any commits 208 if len(commits) > 0 { 209 log.Printf("Resolving %s", commits.String()) 210 for _, c := range commits { 211 commit := commit{Project: c} 212 info, files, err := report.ResolveCommit(ctx, rtx, &c) 213 if err != nil { 214 log.Fatalf("Failed to resolve commit %s:%s", c.Project, c.Revision) 215 } 216 commit.Commit = info 217 rsp.Commits = append(rsp.Commits, commit) 218 219 // Add files to list of inputs 220 rsp.Inputs = append(rsp.Inputs, files...) 221 } 222 addProfileData("Commit Resolution") 223 } 224 225 // Run any sub tools 226 if subcommand != nil { 227 if err := subcommand.Run(ctx, rtx, rsp); err != nil { 228 log.Fatal(err) 229 } 230 addProfileData(subArgs[0]) 231 } 232 233 buildErrors := 0 234 if *buildPtr { 235 // Only support default builder (non server-client) 236 builder := local.NewNinjaCli(local.DefNinjaExc(), *ninjaDbPtr, ninjaTimeout, buildTimeout, false /*clientMode*/) 237 for _, t := range rsp.Targets { 238 log.Printf("Building %s\n", t) 239 res := builder.Build(ctx, t) 240 addProfileData(fmt.Sprintf("Build %s", t)) 241 log.Printf("%s\n", res.Output) 242 if res.Success != true { 243 buildErrors++ 244 } 245 rsp.BuildFiles = append(rsp.BuildFiles, res) 246 } 247 } 248 249 // Generate report 250 log.Printf("Generating report for targets %s", rsp.Targets) 251 req := &app.ReportRequest{Targets: rsp.Targets} 252 rsp.Report, err = report.RunReport(ctx, rtx, req) 253 addProfileData("Report") 254 if err != nil { 255 log.Fatal(fmt.Sprintf("Report failure <%s>", err)) 256 } 257 258 if *jsonPtr { 259 b, _ := json.MarshalIndent(rsp, "", "\t") 260 if *outputPtr == "" { 261 os.Stdout.Write(b) 262 } else { 263 os.WriteFile(*outputPtr, b, 0644) 264 } 265 } else { 266 if *outputPtr == "" { 267 printTextReport(os.Stdout, subcommand, rsp, *verbosePtr) 268 } else { 269 file, err := os.Create(*outputPtr) 270 if err != nil { 271 log.Fatalf("Failed to create output file %s (%s)", *outputPtr, err) 272 } 273 w := bufio.NewWriter(file) 274 printTextReport(w, subcommand, rsp, *verbosePtr) 275 w.Flush() 276 } 277 278 } 279 280 if buildErrors > 0 { 281 log.Fatal(fmt.Sprintf("Failed to build %d targets", buildErrors)) 282 } 283} 284 285func printTextReport(w io.Writer, subcommand tool, rsp *response, verbose bool) { 286 fmt.Fprintln(w, "Metric Report") 287 if subcommand != nil { 288 subcommand.PrintText(w, rsp, verbose) 289 } 290 291 if len(rsp.Commits) > 0 { 292 fmt.Fprintln(w, "") 293 fmt.Fprintln(w, " Commit Results") 294 for _, c := range rsp.Commits { 295 fmt.Fprintf(w, " %-120s : %s\n", c.Project.Project, c.Project.Revision) 296 fmt.Fprintf(w, " SHA : %s\n", c.Commit.Sha) 297 fmt.Fprintf(w, " Files : \n") 298 for _, f := range c.Commit.Files { 299 fmt.Fprintf(w, " %s %s\n", f.Type.String(), f.Filename) 300 } 301 } 302 } 303 if len(rsp.BuildFiles) > 0 { 304 fmt.Fprintln(w, "") 305 fmt.Fprintln(w, " Build Files") 306 for _, b := range rsp.BuildFiles { 307 fmt.Fprintf(w, " %-120s : %t \n", b.Name, b.Success) 308 } 309 } 310 311 targetPrint := func(target *app.BuildTarget) { 312 fmt.Fprintf(w, " %-20s : %s\n", "Name", target.Name) 313 fmt.Fprintf(w, " %-20s : %d\n", "Build Steps", target.Steps) 314 fmt.Fprintf(w, " %-20s \n", "Inputs") 315 fmt.Fprintf(w, " %-20s : %d\n", "Files", target.FileCount) 316 fmt.Fprintf(w, " %-20s : %d\n", "Projects", len(target.Projects)) 317 fmt.Fprintln(w) 318 for name, proj := range target.Projects { 319 forkCount := 0 320 for _, file := range proj.Files { 321 if file.BranchDiff != nil { 322 forkCount++ 323 } 324 } 325 fmt.Fprintf(w, " %-120s : %d ", name, len(proj.Files)) 326 if forkCount != 0 { 327 fmt.Fprintf(w, " (%d)\n", forkCount) 328 } else { 329 fmt.Fprintf(w, " \n") 330 } 331 332 if verbose { 333 for _, file := range proj.Files { 334 var fork string 335 if file.BranchDiff != nil { 336 fork = fmt.Sprintf("(%d+ %d-)", file.BranchDiff.AddedLines, file.BranchDiff.DeletedLines) 337 } 338 fmt.Fprintf(w, " %-20s %s\n", fork, file.Filename) 339 } 340 341 } 342 } 343 344 } 345 fmt.Fprintln(w, " Targets") 346 for _, t := range rsp.Report.Targets { 347 targetPrint(t) 348 } 349 350 fmt.Fprintln(w, " Run Times") 351 for _, p := range rsp.Profile { 352 fmt.Fprintf(w, " %-30s : %f secs\n", p.Description, p.DurationSecs) 353 } 354 355} 356