| // Copyright 2021 The Fuchsia Authors. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "os" |
| |
| "github.com/maruel/subcommands" |
| "go.chromium.org/luci/auth" |
| buildbucketpb "go.chromium.org/luci/buildbucket/proto" |
| "go.chromium.org/luci/luciexe/exe" |
| |
| "go.fuchsia.dev/infra/cmd/size_check/sizes" |
| "go.fuchsia.dev/infra/cmd/size_diff/diff" |
| ) |
| |
| // The exit code to emit when the CI build does not have Buildbucket status |
| // SUCCESS. Exit code 2 is avoided as this would clobber with the exit code |
| // returned upon hitting a panic. |
| const buildNotSuccessfulExitCode = 20 |
| |
| type buildNotSuccessfulError struct { |
| msg string |
| status buildbucketpb.Status |
| } |
| |
| func (e buildNotSuccessfulError) Error() string { return e.msg } |
| |
| func cmdCI(authOpts auth.Options) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "ci -gitiles-remote <gitiles-remote> -base-commit <sha1> -builder <project/bucket/builder> -binary-sizes-json-input <binary-sizes-json-input> -json-output <json-output>", |
| ShortDesc: "Compute diff of the input binary sizes object against a binary sizes object from CI.", |
| LongDesc: "Compute diff of the input binary sizes object against a binary sizes object from CI.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &ciRun{} |
| c.Init(authOpts) |
| return c |
| }, |
| } |
| } |
| |
| type ciRun struct { |
| commonFlags |
| binarySizesJSONInput string |
| } |
| |
| func (c *ciRun) Init(defaultAuthOpts auth.Options) { |
| c.commonFlags.Init(defaultAuthOpts) |
| c.Flags.StringVar(&c.binarySizesJSONInput, "binary-sizes-json-input", "", "Path for input binary sizes object as JSON.") |
| } |
| |
| func (c *ciRun) Parse() error { |
| if err := c.commonFlags.Parse(); err != nil { |
| return err |
| } |
| if c.binarySizesJSONInput == "" { |
| return errors.New("-binary-sizes-json-input is required") |
| } |
| return nil |
| } |
| |
| func (c *ciRun) main() error { |
| ctx := context.Background() |
| fieldMaskPaths1 := []string{ |
| "builds.*.id", |
| "builds.*.status", |
| "builds.*.output.properties.fields.binary_sizes", |
| } |
| fieldMaskPaths2 := []string{ |
| "id", |
| "status", |
| "output.properties.fields.binary_sizes", |
| } |
| build, err := getBuild(ctx, c.commonFlags, fieldMaskPaths1, fieldMaskPaths2) |
| if err != nil { |
| return err |
| } |
| buildLink := fmt.Sprintf("https://%s/build/%d", c.bbHost, build.Id) |
| if build.Status != buildbucketpb.Status_SUCCESS { |
| return buildNotSuccessfulError{ |
| msg: fmt.Sprintf("a successful build is needed to perform the size diff but got status %s, see %s", build.Status, buildLink), |
| status: build.Status, |
| } |
| } |
| |
| var rawCIBinarySizes map[string]any |
| exe.ParseProperties(build.Output.Properties, map[string]any{ |
| "binary_sizes": &rawCIBinarySizes, |
| }) |
| if len(rawCIBinarySizes) == 0 { |
| return fmt.Errorf("binary_sizes output property is not set, see %s", buildLink) |
| } |
| ciBinarySizes, err := sizes.Parse(rawCIBinarySizes) |
| if err != nil { |
| return err |
| } |
| |
| // Read binary sizes JSON input. |
| jsonInput, err := os.ReadFile(c.binarySizesJSONInput) |
| if err != nil { |
| return err |
| } |
| var rawBinarySizes map[string]any |
| if err := json.Unmarshal(jsonInput, &rawBinarySizes); err != nil { |
| return err |
| } |
| binarySizes, err := sizes.Parse(rawBinarySizes) |
| if err != nil { |
| return err |
| } |
| |
| diff := diff.DiffBinarySizes(binarySizes, ciBinarySizes) |
| diff.BaselineBuildID = build.Id |
| |
| // Emit diff to -json-output. |
| out := os.Stdout |
| if c.jsonOutput != "-" { |
| out, err = os.Create(c.jsonOutput) |
| if err != nil { |
| return err |
| } |
| defer out.Close() |
| } |
| data, err := json.MarshalIndent(diff, "", " ") |
| if err != nil { |
| return fmt.Errorf("failed to marshal JSON: %w", err) |
| } |
| _, err = out.Write(data) |
| return err |
| } |
| |
| func (c *ciRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if err := c.Parse(); err != nil { |
| fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err) |
| return 1 |
| } |
| |
| if err := c.main(); err != nil { |
| fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err) |
| var maybeBuildNotSuccessfulError buildNotSuccessfulError |
| if errors.As(err, &maybeBuildNotSuccessfulError) { |
| return buildNotSuccessfulExitCode |
| } |
| return 1 |
| } |
| return 0 |
| } |