go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/recipe_wrapper/main.go (about)

     1  // Copyright 2019 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"os"
    13  	"os/exec"
    14  	"strings"
    15  
    16  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    17  	"go.chromium.org/luci/common/logging"
    18  	"go.chromium.org/luci/common/logging/gologger"
    19  	"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
    20  	"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
    21  	"go.chromium.org/luci/luciexe"
    22  	"google.golang.org/protobuf/proto"
    23  
    24  	"go.fuchsia.dev/infra/cmd/recipe_wrapper/cipd"
    25  	"go.fuchsia.dev/infra/cmd/recipe_wrapper/props"
    26  	"go.fuchsia.dev/infra/cmd/recipe_wrapper/recipes"
    27  )
    28  
    29  // environment represents the environment within which a recipe is run.
    30  type environment struct {
    31  	environ    []string
    32  	recipesExe recipes.Checkout
    33  	buildProto *buildbucketpb.Build
    34  }
    35  
    36  // outputBuildSummary outputs error details to the current build's
    37  // summary_markdown in case of failure.
    38  func outputBuildSummary(ctx context.Context, buildErr error) error {
    39  	bootstrap, err := bootstrap.Get()
    40  	if err != nil {
    41  		return err
    42  	}
    43  	stream, err := bootstrap.Client.NewDatagramStream(
    44  		ctx,
    45  		luciexe.BuildProtoStreamSuffix,
    46  		streamclient.WithContentType(luciexe.BuildProtoContentType),
    47  	)
    48  	if err != nil {
    49  		return err
    50  	}
    51  	defer stream.Close()
    52  	build := &buildbucketpb.Build{}
    53  	build.SummaryMarkdown = buildErr.Error()
    54  	// Check if the returned error declares whether or not it's an infra
    55  	// failure. If it doesn't declare one way or the other, we'll assume that it
    56  	// is an infra failure.
    57  	var maybeInfraErr interface{ IsInfraFailure() bool }
    58  	if errors.As(buildErr, &maybeInfraErr) && !maybeInfraErr.IsInfraFailure() {
    59  		build.Status = buildbucketpb.Status_FAILURE
    60  	} else {
    61  		build.Status = buildbucketpb.Status_INFRA_FAILURE
    62  	}
    63  
    64  	outputData, err := proto.Marshal(build)
    65  	if err != nil {
    66  		return fmt.Errorf("failed to marshal output build.proto: %w", err)
    67  	}
    68  	return stream.WriteDatagram(outputData)
    69  }
    70  
    71  // initEnvironment sets up the environment with recipes, Python 2 etc.
    72  // Returns particulars about the environment created as well as a cleanup function
    73  // that the caller should run when the environment is no longer needed.
    74  func initEnvironment(ctx context.Context) (*environment, func(), error) {
    75  	cwd, err := os.Getwd()
    76  	if err != nil {
    77  		return nil, nil, err
    78  	}
    79  	recipesDir, err := os.MkdirTemp(cwd, "recipes")
    80  	if err != nil {
    81  		return nil, nil, err
    82  	}
    83  
    84  	exe, build, err := recipes.SetUp(ctx, recipesDir)
    85  	if err != nil {
    86  		if outputErr := outputBuildSummary(ctx, err); outputErr != nil {
    87  			logging.Errorf(ctx, "Failed to output build summary after recipe setup failed: %s", outputErr)
    88  		}
    89  		return nil, nil, err
    90  	}
    91  
    92  	// TODO(fxbug.dev/89307): Remove support for Python 2 once branches prior to
    93  	// f11 are no longer supported.
    94  	// Ideally this input property would be read in main() only (same as flags),
    95  	// but at this point it's simpler not to worry about it and just wait to
    96  	// delete this whole block.
    97  	noPy2, err := props.Bool(build, "no_python2")
    98  	if err != nil {
    99  		return nil, nil, err
   100  	}
   101  
   102  	pkgsToInstall := []cipd.Package{}
   103  	rootBinDir, err := os.MkdirTemp("", "recipe_wrapper")
   104  	if err != nil {
   105  		return nil, nil, err
   106  	}
   107  	if noPy2 {
   108  		logging.Infof(ctx, "no_python2=true, not installing python 2")
   109  	} else {
   110  		logging.Infof(ctx, "no_python2=false, installing python 2")
   111  		pkgsToInstall = append(pkgsToInstall, cipd.VPythonPkg, cipd.CPythonPkg)
   112  	}
   113  	binDirs, err := cipd.Install(ctx, rootBinDir, pkgsToInstall...)
   114  	if err != nil {
   115  		return nil, nil, err
   116  	}
   117  	path := strings.Join(append(binDirs, os.Getenv("PATH")), string(os.PathListSeparator))
   118  	os.Setenv("PATH", path)
   119  
   120  	logging.Infof(ctx, "Initialized execution environment to:\n%+v", os.Environ())
   121  
   122  	return &environment{
   123  			environ:    os.Environ(),
   124  			recipesExe: exe,
   125  			buildProto: build,
   126  		}, func() {
   127  			// No need to check errors here, trashing temp files is best effort.
   128  			os.RemoveAll(recipesDir)
   129  			os.RemoveAll(rootBinDir)
   130  		}, err
   131  }
   132  
   133  func runRecipe(ctx context.Context, env *environment) error {
   134  	commandLine, err := env.recipesExe.LuciexeCommand()
   135  	if err != nil {
   136  		return err
   137  	}
   138  	// Forward any flags such as --output to the recipe.
   139  	commandLine = append(commandLine, os.Args[1:]...)
   140  
   141  	cmd := exec.CommandContext(ctx, commandLine[0], commandLine[1:]...)
   142  	cmd.Stdout = os.Stdout
   143  	cmd.Stderr = os.Stderr
   144  	cmd.Env = env.environ
   145  	inputData, err := proto.Marshal(env.buildProto)
   146  	if err != nil {
   147  		return fmt.Errorf("could not marshal input build: %w", err)
   148  	}
   149  	cmd.Stdin = bytes.NewBuffer(inputData)
   150  	logging.Infof(ctx, "Running luciexe command: %s", cmd)
   151  	if err := cmd.Run(); err != nil {
   152  		return fmt.Errorf("failed to execute recipe: %w", err)
   153  	}
   154  	return nil
   155  }
   156  
   157  func joinErrs(errs []error) string {
   158  	var errStrs []string
   159  	for _, err := range errs {
   160  		errStrs = append(errStrs, err.Error())
   161  	}
   162  	return strings.Join(errStrs, ",")
   163  }
   164  
   165  func main() {
   166  	ctx := context.Background()
   167  	ctx = gologger.StdConfig.Use(ctx)
   168  
   169  	logging.Infof(ctx, "Initializing build environment step")
   170  	env, cleanup, err := initEnvironment(ctx)
   171  	if err != nil {
   172  		logging.Errorf(ctx, fmt.Errorf("environment initialization failed: %v", err).Error())
   173  		os.Exit(1)
   174  	}
   175  	defer cleanup()
   176  
   177  	logging.Infof(ctx, "Running recipe step")
   178  	if err := runRecipe(ctx, env); err != nil {
   179  		logging.Errorf(ctx, fmt.Errorf("build failed: %v", err).Error())
   180  		os.Exit(1)
   181  	}
   182  }