blob: ad28ac7b99cc081ec71378024a53edf66d73e797 [file] [log] [blame]
// 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)
}
}