blob: caf28113485b281aa09aa88c9b1d9088828d67cf [file] [log] [blame]
package main
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"sync"
"time"
"cloud.google.com/go/datastore"
"cloud.google.com/go/storage"
"github.com/cenkalti/backoff"
"go.skia.org/infra/go/android_skia_checkout"
"go.skia.org/infra/go/cleanup"
sk_exec "go.skia.org/infra/go/exec"
"go.skia.org/infra/go/fileutil"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/timer"
"go.skia.org/infra/go/util"
)
const (
CHECKOUTS_TOPLEVEL_DIR = "checkouts"
CHECKOUT_DIR_PREFIX = "checkout"
ANDROID_MANIFEST_URL = "https://googleplex-android.googlesource.com/a/platform/manifest"
ANDROID_SKIA_URL = "https://googleplex-android.googlesource.com/a/platform/external/skia"
SKIA_REPO_URL = "http://skia.googlesource.com/a/skia.git"
TRY_BRANCH_NAME = "try_branch"
COMPILE_TASK_LOGS_BUCKET = "android-compile-logs"
// If the mirror sync time exceeds this, then the mirror is recreated
// before the next sync. More context is in skbug.com/8053
MAX_MIRROR_SYNC_TIME_BEFORE_RECREATION = time.Minute * 25
// Exponential backoff values used to retry the syncing of the checkout.
INITIAL_INTERVAL = 10 * time.Second
RANDOMIZATION_FACTOR = 0.5
BACKOFF_MULTIPLIER = 2
MAX_INTERVAL = 3 * time.Minute
MAX_ELAPSED_TIME = 5 * time.Minute
// The below example uses above values to demonstrate what a series of
// retries would look like. retry_interval is 10 seconds,
// randomization_factor is 0.5, multiplier is 2 and the max_interval is 5
// minutes. For 5 tries the sequence will be (values in seconds) and
// assuming we go over the max_elapsed_time on the 5th try:
//
// attempt# retry_interval randomized_interval
// 1 10 [5, 15]
// 2 20 [10, 30]
// 3 40 [20, 60]
// 4 60 [30, 90]
// 5 120 [60, 180]
DEFAULT_LUNCH_TARGET = "cf_x86_phone-eng"
DEFAULT_MMMA_TARGETS = "frameworks/base/core/jni,external/skia"
)
var (
availableCheckoutsChan chan string
bucketHandle *storage.BucketHandle
// Create exponential backoff config.
backOffClient = &backoff.ExponentialBackOff{
InitialInterval: INITIAL_INTERVAL,
RandomizationFactor: RANDOMIZATION_FACTOR,
Multiplier: BACKOFF_MULTIPLIER,
MaxInterval: MAX_INTERVAL,
MaxElapsedTime: MAX_ELAPSED_TIME,
Clock: backoff.SystemClock,
}
gerritClient *gerrit.Gerrit
repoToolPath string
pathToMirror string
// RWMutex for handling checkouts. The mirror will acquire a Lock()
// while the other local checkouts will acquire a RLock().
checkoutsMutex sync.RWMutex
// Whether the mirror should be recreated before it is synced.
recreateMirror bool
)
func CheckoutsInit(numCheckouts int, workdir string, repoUpdateDuration time.Duration, storageClient *storage.Client) error {
user, err := user.Current()
if err != nil {
return err
}
repoToolPath = path.Join(user.HomeDir, "bin", "repo")
ctx := context.Background()
// Make sure ccache directory exists.
pathToCcache := filepath.Join(workdir, "ccache")
if _, err := os.Stat(pathToCcache); err != nil {
if os.IsNotExist(err) {
if _, err := fileutil.EnsureDirExists(pathToCcache); err != nil {
return fmt.Errorf("Error creating %s: %s", pathToCcache, err)
}
}
}
// Make sure the mirror directory is created and initialized.
pathToMirror = filepath.Join(workdir, CHECKOUTS_TOPLEVEL_DIR, "mirror")
if _, err := os.Stat(pathToMirror); err != nil {
if os.IsNotExist(err) {
if err := createMirrorAndInit(ctx, pathToMirror); err != nil {
return fmt.Errorf("Error creating mirror in %s: %s", pathToMirror, err)
}
}
}
// Update mirror here and then periodically.
cleanup.Repeat(repoUpdateDuration, func() {
if err := updateCheckout(ctx, pathToMirror, true); err != nil {
sklog.Errorf("Error when updating the mirror: %s", err)
mirrorSyncFailureMetric.Update(1)
} else {
mirrorSyncFailureMetric.Update(0)
}
}, nil)
// Slice that will be used to update all checkouts in parallel.
checkoutsToUpdate := []string{}
// Channel that will be used to determine which checkouts are available.
availableCheckoutsChan = make(chan string, numCheckouts)
// Populate the channel with available checkouts.
pathToMirrorManifest := filepath.Join(pathToMirror, "platform", "manifest.git")
for i := 1; i <= numCheckouts; i++ {
checkoutPath := filepath.Join(workdir, CHECKOUTS_TOPLEVEL_DIR, fmt.Sprintf("%s_%d", CHECKOUT_DIR_PREFIX, i))
if _, err := os.Stat(checkoutPath); err != nil {
if os.IsNotExist(err) {
if _, err := fileutil.EnsureDirExists(checkoutPath); err != nil {
return fmt.Errorf("Error creating %s: %s", checkoutPath, err)
}
// Make sure mirror is done updating before running init on checkouts.
checkoutsMutex.RLock()
if err := runInit(ctx, checkoutPath, pathToMirrorManifest, false); err != nil {
checkoutsMutex.RUnlock()
return fmt.Errorf("Error running init on %s: %s", checkoutPath, err)
}
checkoutsMutex.RUnlock()
}
}
checkoutsToUpdate = append(checkoutsToUpdate, checkoutPath)
addToCheckoutsChannel(checkoutPath)
}
// Update all checkouts simultaneously.
if err := updateCheckoutsInParallel(ctx, checkoutsToUpdate); err != nil {
return fmt.Errorf("Error when updating checkouts in parallel: %s", err)
}
// Create a Gerrit client.
gerritClient, err = gerrit.NewGerrit(gerrit.GERRIT_SKIA_URL, "", nil)
if err != nil {
return fmt.Errorf("Failed to create a Gerrit client: %s", err)
}
// Get a handle to the Google Storage bucket that logs will be stored in.
bucketHandle = storageClient.Bucket(COMPILE_TASK_LOGS_BUCKET)
return nil
}
func createMirrorAndInit(ctx context.Context, pathToMirror string) error {
if _, err := fileutil.EnsureDirExists(pathToMirror); err != nil {
return fmt.Errorf("Error creating %s: %s", pathToMirror, err)
}
if err := runInit(ctx, pathToMirror, ANDROID_MANIFEST_URL, true); err != nil {
return fmt.Errorf("Error running init on %s: %s", pathToMirror, err)
}
return nil
}
// UpdateCheckoutsInParallel updates all specified checkouts in parallel.
func updateCheckoutsInParallel(ctx context.Context, checkouts []string) error {
sklog.Infof("About to update %d checkouts in parallel.", len(checkouts))
sklog.Info("If the server has no existing checkouts then this step could take a while...")
group := util.NewNamedErrGroup()
for _, checkout := range checkouts {
c := checkout // https://golang.org/doc/faq#closures_and_goroutines
// Create and run a goroutine closure that updates the checkout.
group.Go(c, func() error {
// Make sure the Skia checkout (if it exists) is without any modifications
// from a previous interrupted run.
skiaPath := filepath.Join(c, "external", "skia")
if stat, err := os.Stat(skiaPath); err == nil && stat.IsDir() {
skiaCheckout, err := git.NewCheckout(ctx, ANDROID_SKIA_URL, path.Dir(skiaPath))
if err != nil {
return fmt.Errorf("Failed to create GitDir from %s: %s", skiaPath, err)
}
if err := cleanSkiaCheckout(ctx, skiaCheckout, c); err != nil {
return fmt.Errorf("Error when cleaning Skia checkout at %s: %s", skiaCheckout.Dir(), err)
}
}
// Now update the Android checkout.
if err := updateCheckout(ctx, c, false); err != nil {
return fmt.Errorf("Error when updating checkout in %s: %s", c, err)
}
return nil
})
}
if err := group.Wait(); err != nil {
return err
}
sklog.Infof("Done updating %d checkouts in parallel.", len(checkouts))
return nil
}
func runInit(ctx context.Context, checkoutPath, initRepo string, isMirror bool) error {
initCmd := []string{repoToolPath, "init", "-u", initRepo, "-g", "all,-notdefault,-darwin", "-b", "master"}
if isMirror {
initCmd = append(initCmd, "--mirror")
}
if _, err := sk_exec.RunCwd(ctx, checkoutPath, initCmd...); err != nil {
errMsg := fmt.Sprintf("Failed to init the repo at %s: %s", checkoutPath, err)
sklog.Errorln(errMsg)
return errors.New(errMsg)
}
return nil
}
// updateCheckout updates the Android checkout using the repo tool in the
// specified checkout. Errors are retried with exponential backoff using the
// values in constants.
func updateCheckout(ctx context.Context, checkoutPath string, isMirror bool) error {
if isMirror {
checkoutsMutex.Lock()
defer checkoutsMutex.Unlock()
} else {
checkoutsMutex.RLock()
defer checkoutsMutex.RUnlock()
}
if isMirror && recreateMirror {
sklog.Info("Recreating the mirror.")
util.RemoveAll(checkoutPath)
if err := createMirrorAndInit(ctx, checkoutPath); err != nil {
sklog.Errorf("Error creating mirror in %s: %s", checkoutPath, err)
}
} else {
// Clean checkout before syncing.
pathToCleanCheckoutScript := filepath.Join(*resourcesDir, "clean-checkout.sh")
cleanCheckoutCmd := fmt.Sprintf("bash %s %s", pathToCleanCheckoutScript, checkoutPath)
sklog.Infof("Running %s", cleanCheckoutCmd)
cleanCheckoutOutput, err := sk_exec.RunSimple(ctx, cleanCheckoutCmd)
if err != nil {
errMsg := fmt.Sprintf("Failed to clean checkout: %s", err)
sklog.Errorln(errMsg)
return errors.New(errMsg)
}
sklog.Infof("Output: %s", cleanCheckoutOutput)
}
updateFunc := func() error {
checkoutBase := path.Base(checkoutPath)
sklog.Infof("Started updating %s", checkoutBase)
// Create metric and send it to a timer.
syncTimesMetric := metrics2.GetFloat64Metric(fmt.Sprintf("android_sync_time_%s", checkoutBase))
timerMetric := timer.NewWithMetric(fmt.Sprintf("Time taken to update %s:", checkoutBase), syncTimesMetric)
defer func() {
duration := timerMetric.Stop()
if isMirror {
if recreateMirror {
// We did the first sync of a recreated mirror. The first sync takes longer than usual
// thus we do not need to check if duration exceeded MAX_MIRROR_SYNC_TIME_BEFORE_RECREATION.
// Initialize recreateMirror back to false.
recreateMirror = false
} else {
recreateMirror = duration > MAX_MIRROR_SYNC_TIME_BEFORE_RECREATION
}
if recreateMirror {
sklog.Warningf("Mirror sync time %s was greater than %s", duration, MAX_MIRROR_SYNC_TIME_BEFORE_RECREATION)
sklog.Info("Will recreate mirror before next sync.")
}
}
}()
// Sync the current branch, only fetch projects fixed to sha1 if revision
// does not exist locally, and delete refs that no longer exist on server.
if _, err := sk_exec.RunCwd(ctx, checkoutPath, repoToolPath, "sync", "-c", "-j25", "--optimized-fetch", "--prune", "-f"); err != nil {
errMsg := fmt.Sprintf("Failed to sync the repo at %s: %s", checkoutBase, err)
sklog.Errorln(errMsg)
return errors.New(errMsg)
}
return nil
}
return backoff.Retry(updateFunc, backOffClient)
}
// applyPatch applies a patch from the specified issue and patchset to the
// specified Skia checkout.
func applyPatch(ctx context.Context, skiaCheckout *git.Checkout, issue, patchset int) error {
// Run a git fetch for the branch where gerrit stores patches.
//
// refs/changes/46/4546/1
// | | |
// | | +-> Patch set.
// | |
// | +-> Issue ID.
// |
// +-> Last two digits of Issue ID.
issuePostfix := issue % 100
if err := skiaCheckout.FetchRefFromRepo(ctx, SKIA_REPO_URL, fmt.Sprintf("refs/changes/%02d/%d/%d", issuePostfix, issue, patchset)); err != nil {
return fmt.Errorf("Failed to fetch ref in %s: %s", skiaCheckout.Dir(), err)
}
if _, err := skiaCheckout.Git(ctx, "reset", "--hard", "FETCH_HEAD"); err != nil {
return fmt.Errorf("Failed to checkout FETCH_HEAD in %s: %s", skiaCheckout.Dir(), err)
}
return nil
}
func deleteTryBranch(ctx context.Context, skiaCheckout *git.Checkout) error {
if _, err := skiaCheckout.Git(ctx, "branch", "-D", TRY_BRANCH_NAME); err != nil {
return fmt.Errorf("Failed to delete branch %s: %s", TRY_BRANCH_NAME, err)
}
return nil
}
func resetSkiaCheckout(ctx context.Context, skiaCheckout *git.Checkout, resetCommit string) error {
if _, err := skiaCheckout.Git(ctx, "rebase", "--abort"); err != nil {
// Do nothing. There was no incomplete rebase.
}
if _, err := skiaCheckout.Git(ctx, "clean", "-d", "-f"); err != nil {
return err
}
if _, err := skiaCheckout.Git(ctx, "reset", "--hard", resetCommit); err != nil {
return err
}
return nil
}
func cleanSkiaCheckout(ctx context.Context, skiaCheckout *git.Checkout, checkoutPath string) error {
if err := resetSkiaCheckout(ctx, skiaCheckout, "HEAD"); err != nil {
return fmt.Errorf("Error when resetting Skia checkout: %s", err)
}
// Checkout Android's built in remote "goog/master" incase the checkout is
// tracking something else.
if _, err := skiaCheckout.Git(ctx, "checkout", fmt.Sprintf("%s/master", android_skia_checkout.BUILT_IN_REMOTE)); err != nil {
return fmt.Errorf("Failed to checkout goog/master in %s: %s", checkoutPath, err)
}
if err := deleteTryBranch(ctx, skiaCheckout); err != nil {
// Do nothing. The try branch does not exist yet which is ok.
}
return nil
}
func checkPatchFromMasterBranch(issue int) (bool, error) {
changeInfo, err := gerritClient.GetIssueProperties(int64(issue))
if err != nil {
return false, fmt.Errorf("Could not get properties of Gerrit issue %d: %s", issue, err)
}
return changeInfo.Branch == "master", nil
}
func checkCommitFromMasterBranch(ctx context.Context, skiaCheckout *git.Checkout) (bool, error) {
// Check that current HEAD is from origin/master and not any other branch.
fromMaster, err := skiaCheckout.IsAncestor(ctx, "HEAD", "origin/master")
if err != nil {
return false, fmt.Errorf("Could not find ancestor in %s: %s", skiaCheckout.Dir(), err)
}
return fromMaster, nil
}
func prepareSkiaCheckoutForCompile(ctx context.Context, userConfigContent []byte, skiaCheckout *git.Checkout) error {
skiaPath := skiaCheckout.Dir()
if err := android_skia_checkout.RunGnToBp(ctx, skiaPath); err != nil {
return fmt.Errorf("Error when running gn_to_bp: %s", err)
}
// TODO(rmistry): Cannot compile Android with third_party/externals. Find
// a relatively less hackier way around this.
if err := os.RemoveAll(filepath.Join(skiaPath, "third_party", "externals")); err != nil {
// go back to previous state.
return fmt.Errorf("Error when deleting third_party/externals: %s", err)
}
// Create SkUserConfigManual.h file since it is required for compilation.
f, err := os.Create(filepath.Join(skiaPath, android_skia_checkout.SkUserConfigManualRelPath))
if err != nil {
return fmt.Errorf("Could not create %s: %s", android_skia_checkout.SkUserConfigManualRelPath, err)
}
defer util.Close(f)
if _, err := f.Write(userConfigContent); err != nil {
return fmt.Errorf("Could not write to %s: %s", android_skia_checkout.SkUserConfigManualRelPath, err)
}
// Remove SkUserConfig.h so it does not cause failures, see
// https://skia-review.googlesource.com/c/skia/+/171231 for context.
if err := os.Remove(filepath.Join(skiaPath, android_skia_checkout.SkUserConfigRelPath)); err != nil {
return fmt.Errorf("Error when deleting %s: %s", android_skia_checkout.SkUserConfigRelPath, err)
}
return nil
}
func addToCheckoutsChannel(checkout string) {
availableCheckoutsChan <- checkout
}
// RunCompileTask runs the specified CompileTask using the following algorithm-
//
// Step 1: Find an available Android checkout and update the CompileTask with
// the checkout. This is done for the UI and for easier debugging.
//
// Step 2: Make sure the Skia checkout within Android is clean. We do this
// before updating to be extra careful and make the server more robust.
//
// Step 3: Update the Android checkout.
//
// Step 4: Get the Android hash currently in Skia's checkout. We will
// go back to this hash after we are done updating to Skia master/origin
// and compiling.
//
// Step 5: Create a branch and have it track origin/master.
//
// Step 6: If it is a trybot run then apply the patch else apply the hash.
//
// Step 7: Prepare the Skia checkout for compilation: Run gn_to_bp.py and
// create SkUserConfigManual.h from Step 5.
//
// Step 8: Do the with patch or with hash compilation and update CompileTask
// with link to logs and whether it was successful.
//
// Step 9: If the compilation failed and if it is a trybot run then verify
// that the tree is not broken by building at Skia HEAD. Update CompileTask
// with link to logs and whether the no patch run was successful.
//
func RunCompileTask(ctx context.Context, g *gsFileLocation, task *CompileTask, datastoreKey *datastore.Key, pathToCompileScript string) error {
incWaitingMetric()
// Blocking call to wait for an available checkout.
checkoutPath := <-availableCheckoutsChan
moveToRunningMetric()
defer decRunningMetric()
defer addToCheckoutsChannel(checkoutPath)
// Step 1: Find an available Android checkout and update the CompileTask
// with the checkout. This is done for the UI and for easier debugging.
task.Checkout = path.Base(checkoutPath)
if err := UpdateCompileTask(ctx, g, datastoreKey, task); err != nil {
return fmt.Errorf("Could not update compile task with ID %d: %s", datastoreKey.ID, err)
}
skiaPath := filepath.Join(checkoutPath, "external", "skia")
skiaCheckout, err := git.NewCheckout(ctx, ANDROID_SKIA_URL, path.Dir(skiaPath))
if err != nil {
return fmt.Errorf("Failed to create GitDir from %s: %s", skiaPath, err)
}
// Step 2: Make sure the Skia checkout within Android is clean. We do this
// before updating to be extra careful and make the server more robust.
if err := cleanSkiaCheckout(ctx, skiaCheckout, checkoutPath); err != nil {
return fmt.Errorf("Error when cleaning Skia checkout: %s", err)
}
// Step 3: Update the Android checkout.
if err := updateCheckout(ctx, checkoutPath, false); err != nil {
return fmt.Errorf("Error when updating checkout in %s: %s", checkoutPath, err)
}
// Step 4: Get contents of SkUserConfigManual.h. We will use this after
// updating Skia to master/origin and before compiling.
userConfigContent, err := ioutil.ReadFile(filepath.Join(skiaPath, android_skia_checkout.SkUserConfigManualRelPath))
if err != nil {
return fmt.Errorf("Could not read from %s: %s", android_skia_checkout.SkUserConfigManualRelPath, err)
}
// Add origin remote that points to the Skia repo.
if err := skiaCheckout.AddRemote(ctx, "origin", SKIA_REPO_URL); err != nil {
return fmt.Errorf("Error when adding origin remote: %s", err)
}
// Fetch origin without updating checkout
if err := skiaCheckout.Fetch(ctx); err != nil {
return fmt.Errorf("Error when fetching origin: %s", err)
}
// Step 5: Create a branch and have it track origin/master.
if _, err := skiaCheckout.Git(ctx, "checkout", "-b", TRY_BRANCH_NAME, "-t", "origin/master"); err != nil {
return fmt.Errorf("Error when creating %s in %s: %s", TRY_BRANCH_NAME, skiaPath, err)
}
// Step 6: If it is a trybot run then apply the patch else apply the hash.
trybotRun := (task.Hash == "")
if trybotRun {
// Apply Patch.
if err := applyPatch(ctx, skiaCheckout, task.Issue, task.PatchSet); err != nil {
return fmt.Errorf("Could not apply the patch with issue %d and patchset %d: %s", task.Issue, task.PatchSet, err)
}
// Check to see if patch is from origin/master.
fromMaster, err := checkPatchFromMasterBranch(task.Issue)
if err != nil {
return fmt.Errorf("Could not check if commit is from origin/master: %s", err)
}
task.IsMasterBranch = fromMaster
if !task.IsMasterBranch {
if err := UpdateCompileTask(ctx, g, datastoreKey, task); err != nil {
return fmt.Errorf("Could not update compile task with ID %d: %s", datastoreKey.ID, err)
}
sklog.Infof("Patch with issue %d and patchset %d is not on master branch.", task.Issue, task.PatchSet)
return nil
}
// Rebase the checkout after applying the patch.
if _, err := skiaCheckout.Git(ctx, "rebase"); err != nil {
return fmt.Errorf("Failed to rebase in %s: %s", skiaCheckout.Dir(), err)
}
} else {
// Checkout the specified Skia hash.
// TODO(rmistry): This has lots of problems, the non-trybot bot could fail if
// Android tree is red. Maybe non-trybot path should not be supported?
if _, err := skiaCheckout.Git(ctx, "checkout", task.Hash); err != nil {
return fmt.Errorf("Failed to checkout Skia hash %s: %s", task.Hash, err)
}
// Check to see if hash is from origin/master.
fromMaster, err := checkCommitFromMasterBranch(ctx, skiaCheckout)
if err != nil {
return fmt.Errorf("Could not check if commit is from origin/master: %s", err)
}
task.IsMasterBranch = fromMaster
if !task.IsMasterBranch {
if err := UpdateCompileTask(ctx, g, datastoreKey, task); err != nil {
return fmt.Errorf("Could not update compile task with ID %d: %s", datastoreKey.ID, err)
}
sklog.Infof("Hash %s is not on master branch.", task.Hash)
return nil
}
}
// Step 7: Prepare the Skia checkout for compilation: Run gn_to_bp.py and
// create SkUserConfigManual.h from Step 5.
if err := prepareSkiaCheckoutForCompile(ctx, userConfigContent, skiaCheckout); err != nil {
return fmt.Errorf("Could not prepare Skia checkout for compile: %s", err)
}
// Step 8: Do the with patch or with hash compilation and update CompileTask
// with link to logs and whether it was successful.
withPatchSuccess, gsWithPatchLink, err := compileCheckout(ctx, checkoutPath, task.LunchTarget, task.MMMATargets, fmt.Sprintf("%d_withpatch_", datastoreKey.ID), pathToCompileScript)
if err != nil {
return fmt.Errorf("Error when compiling checkout withpatch at %s: %s", checkoutPath, err)
}
task.WithPatchSucceeded = withPatchSuccess
task.WithPatchLog = gsWithPatchLink
if err := UpdateCompileTask(ctx, g, datastoreKey, task); err != nil {
return fmt.Errorf("Could not update compile task with ID %d: %s", datastoreKey.ID, err)
}
// Step 9: If the compilation failed and if it is a trybot run then verify
// that the tree is not broken by building at Skia HEAD. Update CompileTask
// with link to logs and whether the no patch run was successful.
if !withPatchSuccess && trybotRun {
// If this failed then check to see if a build without the patch will succeed.
if err := resetSkiaCheckout(ctx, skiaCheckout, "origin/master"); err != nil {
return fmt.Errorf("Error when resetting Skia checkout: %s", err)
}
// Checkout origin/master.
if _, err := skiaCheckout.Git(ctx, "checkout", "origin/master"); err != nil {
return fmt.Errorf("Failed to checkout origin/master: %s", err)
}
if err := prepareSkiaCheckoutForCompile(ctx, userConfigContent, skiaCheckout); err != nil {
return fmt.Errorf("Could not prepare Skia checkout for compile: %s", err)
}
// Do the no patch compilation.
noPatchSuccess, gsNoPatchLink, err := compileCheckout(ctx, checkoutPath, task.LunchTarget, task.MMMATargets, fmt.Sprintf("%d_nopatch_", datastoreKey.ID), pathToCompileScript)
if err != nil {
return fmt.Errorf("Error when compiling checkout nopatch at %s: %s", checkoutPath, err)
}
updateAndroidTreeBrokenMetric(!noPatchSuccess)
task.NoPatchSucceeded = noPatchSuccess
task.NoPatchLog = gsNoPatchLink
if err := UpdateCompileTask(ctx, g, datastoreKey, task); err != nil {
return fmt.Errorf("Could not update compile task with ID %d: %s", datastoreKey.ID, err)
}
} else {
// The with patch run succeeded. Mark the android_compile_tree_broken metric accordingly.
updateAndroidTreeBrokenMetric(false)
}
return nil
}
// compileCheckout runs compile.sh which compiles the provided Android checkout.
// Compile logs are then stored in the provided Google storage location with the
// specified prefix. Returns whether the compilation was successful and a link
// to the compilation logs in Google Storage.
// We do the compilation via compile.sh and not via exec because
// ./build/envsetup.sh needs to be sournced before running lunch and mma
// commands and this was much simpler to do via a bash script.
func compileCheckout(ctx context.Context, checkoutPath, lunchTarget, mmmaTargets, logFilePrefix, pathToCompileScript string) (bool, string, error) {
checkoutBase := path.Base(checkoutPath)
sklog.Infof("Started compiling %s", checkoutBase)
// Create metric and send it to a timer.
compileTimesMetric := metrics2.GetFloat64Metric(fmt.Sprintf("android_compile_time_%s", checkoutBase))
defer timer.NewWithMetric(fmt.Sprintf("Time taken to compile %s:", checkoutBase), compileTimesMetric).Stop()
// Execute the compile script pointing it to the checkout.
command := exec.Command("bash", pathToCompileScript, checkoutPath, lunchTarget, mmmaTargets)
logFile, err := ioutil.TempFile(*workdir, logFilePrefix)
defer util.Remove(logFile.Name())
if err != nil {
return false, "", fmt.Errorf("Could not create log file")
}
command.Stdout = io.MultiWriter(logFile, os.Stdout)
command.Stderr = command.Stdout
// Execute the command and determine if it was successful
compileSuccess := (command.Run() == nil)
// Put the log file in Google Storage.
target := bucketHandle.Object(filepath.Base(logFile.Name()))
writer := target.NewWriter(ctx)
writer.ObjectAttrs.ContentType = "text/plain"
// Make uploaded logs readable by google.com domain.
writer.ObjectAttrs.ACL = []storage.ACLRule{{Entity: "domain-google.com", Role: storage.RoleReader}}
defer util.Close(writer)
data, err := ioutil.ReadFile(logFile.Name())
compileLog := string(data)
// Write the logs to Google storage.
if _, err := io.WriteString(writer, compileLog); err != nil {
return compileSuccess, "", fmt.Errorf("Could not write %s to google storage: %s", logFile.Name(), err)
}
// Write to logs to sklog as well.
sklog.Infof("Compilation logs for %s on %s:", logFilePrefix, checkoutBase)
sklog.Infof("\n---------------------------------------------------\n%s\n---------------------------------------------------\n", compileLog)
return compileSuccess, fmt.Sprintf("https://storage.cloud.google.com/%s/%s", COMPILE_TASK_LOGS_BUCKET, filepath.Base(logFile.Name())), nil
}