| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "fmt" |
| "os" |
| "os/exec" |
| "strings" |
| |
| buildbucketpb "go.chromium.org/luci/buildbucket/proto" |
| "go.chromium.org/luci/common/logging" |
| "go.chromium.org/luci/common/logging/gologger" |
| "go.chromium.org/luci/logdog/client/butlerlib/bootstrap" |
| "go.chromium.org/luci/logdog/client/butlerlib/streamclient" |
| "go.chromium.org/luci/luciexe" |
| "google.golang.org/protobuf/proto" |
| |
| "go.fuchsia.dev/infra/cmd/recipe_wrapper/cipd" |
| "go.fuchsia.dev/infra/cmd/recipe_wrapper/props" |
| "go.fuchsia.dev/infra/cmd/recipe_wrapper/recipes" |
| ) |
| |
| // environment represents the environment within which a recipe is run. |
| type environment struct { |
| environ []string |
| recipesExe recipes.Checkout |
| buildProto *buildbucketpb.Build |
| } |
| |
| // outputBuildSummary outputs error details to the current build's |
| // summary_markdown in case of failure. |
| func outputBuildSummary(ctx context.Context, buildErr error) error { |
| bootstrap, err := bootstrap.Get() |
| if err != nil { |
| return err |
| } |
| stream, err := bootstrap.Client.NewDatagramStream( |
| ctx, |
| luciexe.BuildProtoStreamSuffix, |
| streamclient.WithContentType(luciexe.BuildProtoContentType), |
| ) |
| if err != nil { |
| return err |
| } |
| defer stream.Close() |
| build := &buildbucketpb.Build{} |
| build.SummaryMarkdown = buildErr.Error() |
| // Check if the returned error declares whether or not it's an infra |
| // failure. If it doesn't declare one way or the other, we'll assume that it |
| // is an infra failure. |
| var maybeInfraErr interface{ IsInfraFailure() bool } |
| if errors.As(buildErr, &maybeInfraErr) && !maybeInfraErr.IsInfraFailure() { |
| build.Status = buildbucketpb.Status_FAILURE |
| } else { |
| build.Status = buildbucketpb.Status_INFRA_FAILURE |
| } |
| |
| outputData, err := proto.Marshal(build) |
| if err != nil { |
| return fmt.Errorf("failed to marshal output build.proto: %w", err) |
| } |
| return stream.WriteDatagram(outputData) |
| } |
| |
| // initEnvironment sets up the environment with recipes, Python 2 etc. |
| // Returns particulars about the environment created as well as a cleanup function |
| // that the caller should run when the environment is no longer needed. |
| func initEnvironment(ctx context.Context) (*environment, func(), error) { |
| cwd, err := os.Getwd() |
| if err != nil { |
| return nil, nil, err |
| } |
| recipesDir, err := os.MkdirTemp(cwd, "recipes") |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| exe, build, err := recipes.SetUp(ctx, recipesDir) |
| if err != nil { |
| if outputErr := outputBuildSummary(ctx, err); outputErr != nil { |
| logging.Errorf(ctx, "Failed to output build summary after recipe setup failed: %s", outputErr) |
| } |
| return nil, nil, err |
| } |
| |
| // TODO(fxbug.dev/89307): Remove support for Python 2 once branches prior to |
| // f11 are no longer supported. |
| // Ideally this input property would be read in main() only (same as flags), |
| // but at this point it's simpler not to worry about it and just wait to |
| // delete this whole block. |
| noPy2, err := props.Bool(build, "no_python2") |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| pkgsToInstall := []cipd.Package{} |
| rootBinDir, err := os.MkdirTemp("", "recipe_wrapper") |
| if err != nil { |
| return nil, nil, err |
| } |
| if noPy2 { |
| logging.Infof(ctx, "no_python2=true, not installing python 2") |
| } else { |
| logging.Infof(ctx, "no_python2=false, installing python 2") |
| pkgsToInstall = append(pkgsToInstall, cipd.VPythonPkg, cipd.CPythonPkg) |
| } |
| binDirs, err := cipd.Install(ctx, rootBinDir, pkgsToInstall...) |
| if err != nil { |
| return nil, nil, err |
| } |
| path := strings.Join(append(binDirs, os.Getenv("PATH")), string(os.PathListSeparator)) |
| os.Setenv("PATH", path) |
| |
| logging.Infof(ctx, "Initialized execution environment to:\n%+v", os.Environ()) |
| |
| return &environment{ |
| environ: os.Environ(), |
| recipesExe: exe, |
| buildProto: build, |
| }, func() { |
| // No need to check errors here, trashing temp files is best effort. |
| os.RemoveAll(recipesDir) |
| os.RemoveAll(rootBinDir) |
| }, err |
| } |
| |
| func runRecipe(ctx context.Context, env *environment) error { |
| commandLine, err := env.recipesExe.LuciexeCommand() |
| if err != nil { |
| return err |
| } |
| // Forward any flags such as --output to the recipe. |
| commandLine = append(commandLine, os.Args[1:]...) |
| |
| cmd := exec.CommandContext(ctx, commandLine[0], commandLine[1:]...) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| cmd.Env = env.environ |
| inputData, err := proto.Marshal(env.buildProto) |
| if err != nil { |
| return fmt.Errorf("could not marshal input build: %w", err) |
| } |
| cmd.Stdin = bytes.NewBuffer(inputData) |
| logging.Infof(ctx, "Running luciexe command: %s", cmd) |
| if err := cmd.Run(); err != nil { |
| return fmt.Errorf("failed to execute recipe: %w", err) |
| } |
| return nil |
| } |
| |
| func joinErrs(errs []error) string { |
| var errStrs []string |
| for _, err := range errs { |
| errStrs = append(errStrs, err.Error()) |
| } |
| return strings.Join(errStrs, ",") |
| } |
| |
| func main() { |
| ctx := context.Background() |
| ctx = gologger.StdConfig.Use(ctx) |
| |
| logging.Infof(ctx, "Initializing build environment step") |
| env, cleanup, err := initEnvironment(ctx) |
| if err != nil { |
| logging.Errorf(ctx, fmt.Errorf("environment initialization failed: %v", err).Error()) |
| os.Exit(1) |
| } |
| defer cleanup() |
| |
| logging.Infof(ctx, "Running recipe step") |
| if err := runRecipe(ctx, env); err != nil { |
| logging.Errorf(ctx, fmt.Errorf("build failed: %v", err).Error()) |
| os.Exit(1) |
| } |
| } |