| 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 |
| } |