// Copyright 2022 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bufio" "context" "encoding/json" "errors" "flag" "fmt" "io" "log" "os" "runtime" "strings" "time" "tools/treble/build/report/app" "tools/treble/build/report/local" "tools/treble/build/report/report" ) type Build interface { Build(ctx context.Context, target string) *app.BuildCmdResult } type tool interface { Run(ctx context.Context, rtx *report.Context, rsp *response) error PrintText(w io.Writer, rsp *response, verbose bool) } type repoFlags []app.ProjectCommit func (r *repoFlags) Set(value string) error { commit := app.ProjectCommit{} items := strings.Split(value, ":") if len(items) > 2 { return (errors.New("Invalid repo value expected (proj:sha) format")) } commit.Project = items[0] if len(items) > 1 { commit.Revision = items[1] } *r = append(*r, commit) return nil } func (r *repoFlags) String() string { items := []string{} for _, fl := range *r { items = append(items, fmt.Sprintf("%s:%s", fl.Project, fl.Revision)) } return strings.Join(items, " ") } var ( // Common flags ninjaDbPtr = flag.String("ninja", local.DefNinjaDb(), "Set the .ninja file to use when building metrics") ninjaExcPtr = flag.String("ninja_cmd", local.DefNinjaExc(), "Set the ninja executable") ninjaTimeoutStr = flag.String("ninja_timeout", local.DefaultNinjaTimeout, "Default ninja timeout") buildTimeoutStr = flag.String("build_timeout", local.DefaultNinjaBuildTimeout, "Default build timeout") manifestPtr = flag.String("manifest", local.DefManifest(), "Set the location of the manifest file") upstreamPtr = flag.String("upstream", "", "Upstream branch to compare files against") repoBasePtr = flag.String("repo_base", local.DefRepoBase(), "Set the repo base directory") workerCountPtr = flag.Int("worker_count", runtime.NumCPU(), "Number of worker routines") buildWorkerCountPtr = flag.Int("build_worker_count", local.MaxNinjaCliWorkers, "Number of build worker routines") clientServerPtr = flag.Bool("client_server", false, "Run client server mode") buildPtr = flag.Bool("build", false, "Build targets") jsonPtr = flag.Bool("json", false, "Print json data") verbosePtr = flag.Bool("v", false, "Print verbose text data") outputPtr = flag.String("o", "", "Output to file") projsPtr = flag.Bool("projects", false, "Include project repo data") hostFlags = flag.NewFlagSet("host", flag.ExitOnError) queryFlags = flag.NewFlagSet("query", flag.ExitOnError) pathsFlags = flag.NewFlagSet("paths", flag.ExitOnError) ) // Add profiling data type profTime struct { Description string `json:"description"` DurationSecs float64 `json:"duration"` } type commit struct { Project app.ProjectCommit `json:"project"` Commit *app.GitCommit `json:"commit"` } // Use one structure for output for now type response struct { Commits []commit `json:"commits,omitempty"` Inputs []string `json:"files,omitempty"` BuildFiles []*app.BuildCmdResult `json:"build_files,omitempty"` Targets []string `json:"targets,omitempty"` Report *app.Report `json:"report,omitempty"` // Subcommand data Query *app.QueryResponse `json:"query,omitempty"` Paths []*app.BuildPath `json:"build_paths,omitempty"` Host *app.HostReport `json:"host,omitempty"` Projects map[string]*app.GitProject `json:"projects,omitempty"` // Profile data Profile []*profTime `json:"profile"` } func main() { startTime := time.Now() ctx := context.Background() rsp := &response{} var addProfileData = func(desc string) { rsp.Profile = append(rsp.Profile, &profTime{Description: desc, DurationSecs: time.Since(startTime).Seconds()}) startTime = time.Now() } flag.Parse() ninjaTimeout, err := time.ParseDuration(*ninjaTimeoutStr) if err != nil { log.Fatalf("Invalid ninja timeout %s", *ninjaTimeoutStr) } buildTimeout, err := time.ParseDuration(*buildTimeoutStr) if err != nil { log.Fatalf("Invalid build timeout %s", *buildTimeoutStr) } subArgs := flag.Args() defBuildTarget := "droid" log.SetFlags(log.LstdFlags | log.Llongfile) ninja := local.NewNinjaCli(*ninjaExcPtr, *ninjaDbPtr, ninjaTimeout, buildTimeout, *clientServerPtr) if *clientServerPtr { ninjaServ := local.NewNinjaServer(*ninjaExcPtr, *ninjaDbPtr) defer ninjaServ.Kill() go func() { ninjaServ.Start(ctx) }() if err := ninja.WaitForServer(ctx, int(ninjaTimeout.Seconds())); err != nil { log.Fatalf("Failed to connect to server") } } rtx := &report.Context{ RepoBase: *repoBasePtr, Repo: &report.RepoMan{}, Build: ninja, Project: local.NewGitCli(), WorkerCount: *workerCountPtr, BuildWorkerCount: *buildWorkerCountPtr, } var subcommand tool var commits repoFlags if len(subArgs) > 0 { switch subArgs[0] { case "host": hostToolPathPtr := hostFlags.String("hostbin", local.DefHostBinPath(), "Set the output directory for host tools") hostFlags.Parse(subArgs[1:]) subcommand = &hostReport{toolPath: *hostToolPathPtr} rsp.Targets = hostFlags.Args() case "query": queryFlags.Var(&commits, "repo", "Repo:SHA to query") queryFlags.Parse(subArgs[1:]) subcommand = &queryReport{} rsp.Targets = queryFlags.Args() case "paths": pathsFlags.Var(&commits, "repo", "Repo:SHA to build") singlePathPtr := pathsFlags.Bool("1", false, "Get single path to output target") pathsFlags.Parse(subArgs[1:]) subcommand = &pathsReport{build_target: defBuildTarget, single: *singlePathPtr} rsp.Inputs = pathsFlags.Args() default: rsp.Targets = subArgs } } addProfileData("Init") rtx.ResolveProjectMap(ctx, *manifestPtr, *upstreamPtr) addProfileData("Project Map") // Add project to output if requested if *projsPtr == true { rsp.Projects = make(map[string]*app.GitProject) for k, p := range rtx.Info.ProjMap { rsp.Projects[k] = p.GitProj } } // Resolve any commits if len(commits) > 0 { log.Printf("Resolving %s", commits.String()) for _, c := range commits { commit := commit{Project: c} info, files, err := report.ResolveCommit(ctx, rtx, &c) if err != nil { log.Fatalf("Failed to resolve commit %s:%s", c.Project, c.Revision) } commit.Commit = info rsp.Commits = append(rsp.Commits, commit) // Add files to list of inputs rsp.Inputs = append(rsp.Inputs, files...) } addProfileData("Commit Resolution") } // Run any sub tools if subcommand != nil { if err := subcommand.Run(ctx, rtx, rsp); err != nil { log.Fatal(err) } addProfileData(subArgs[0]) } buildErrors := 0 if *buildPtr { // Only support default builder (non server-client) builder := local.NewNinjaCli(local.DefNinjaExc(), *ninjaDbPtr, ninjaTimeout, buildTimeout, false /*clientMode*/) for _, t := range rsp.Targets { log.Printf("Building %s\n", t) res := builder.Build(ctx, t) addProfileData(fmt.Sprintf("Build %s", t)) log.Printf("%s\n", res.Output) if res.Success != true { buildErrors++ } rsp.BuildFiles = append(rsp.BuildFiles, res) } } // Generate report log.Printf("Generating report for targets %s", rsp.Targets) req := &app.ReportRequest{Targets: rsp.Targets} rsp.Report, err = report.RunReport(ctx, rtx, req) addProfileData("Report") if err != nil { log.Fatal(fmt.Sprintf("Report failure <%s>", err)) } if *jsonPtr { b, _ := json.MarshalIndent(rsp, "", "\t") if *outputPtr == "" { os.Stdout.Write(b) } else { os.WriteFile(*outputPtr, b, 0644) } } else { if *outputPtr == "" { printTextReport(os.Stdout, subcommand, rsp, *verbosePtr) } else { file, err := os.Create(*outputPtr) if err != nil { log.Fatalf("Failed to create output file %s (%s)", *outputPtr, err) } w := bufio.NewWriter(file) printTextReport(w, subcommand, rsp, *verbosePtr) w.Flush() } } if buildErrors > 0 { log.Fatal(fmt.Sprintf("Failed to build %d targets", buildErrors)) } } func printTextReport(w io.Writer, subcommand tool, rsp *response, verbose bool) { fmt.Fprintln(w, "Metric Report") if subcommand != nil { subcommand.PrintText(w, rsp, verbose) } if len(rsp.Commits) > 0 { fmt.Fprintln(w, "") fmt.Fprintln(w, " Commit Results") for _, c := range rsp.Commits { fmt.Fprintf(w, " %-120s : %s\n", c.Project.Project, c.Project.Revision) fmt.Fprintf(w, " SHA : %s\n", c.Commit.Sha) fmt.Fprintf(w, " Files : \n") for _, f := range c.Commit.Files { fmt.Fprintf(w, " %s %s\n", f.Type.String(), f.Filename) } } } if len(rsp.BuildFiles) > 0 { fmt.Fprintln(w, "") fmt.Fprintln(w, " Build Files") for _, b := range rsp.BuildFiles { fmt.Fprintf(w, " %-120s : %t \n", b.Name, b.Success) } } targetPrint := func(target *app.BuildTarget) { fmt.Fprintf(w, " %-20s : %s\n", "Name", target.Name) fmt.Fprintf(w, " %-20s : %d\n", "Build Steps", target.Steps) fmt.Fprintf(w, " %-20s \n", "Inputs") fmt.Fprintf(w, " %-20s : %d\n", "Files", target.FileCount) fmt.Fprintf(w, " %-20s : %d\n", "Projects", len(target.Projects)) fmt.Fprintln(w) for name, proj := range target.Projects { forkCount := 0 for _, file := range proj.Files { if file.BranchDiff != nil { forkCount++ } } fmt.Fprintf(w, " %-120s : %d ", name, len(proj.Files)) if forkCount != 0 { fmt.Fprintf(w, " (%d)\n", forkCount) } else { fmt.Fprintf(w, " \n") } if verbose { for _, file := range proj.Files { var fork string if file.BranchDiff != nil { fork = fmt.Sprintf("(%d+ %d-)", file.BranchDiff.AddedLines, file.BranchDiff.DeletedLines) } fmt.Fprintf(w, " %-20s %s\n", fork, file.Filename) } } } } fmt.Fprintln(w, " Targets") for _, t := range rsp.Report.Targets { targetPrint(t) } fmt.Fprintln(w, " Run Times") for _, p := range rsp.Profile { fmt.Fprintf(w, " %-30s : %f secs\n", p.Description, p.DurationSecs) } }