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 }