blob: cfc950b02c3036faf2cc0bba5aa4d715075664ba [file]
package chromiumbuilder
// Code related to ChromiumBuilderService.Init().
import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"strings"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/vfs"
)
const (
ArgDepotToolsPath string = "depot_tools_path"
ArgChromiumPath string = "chromium_path"
)
// initImpl is the actual implementation for Init(), broken out to support
// dependency injection.
func (s *ChromiumBuilderService) initImpl(
ctx context.Context, serviceArgs string, fs vfs.FS, cf checkoutFactory, dc directoryCreator, ccr concurrentCommandRunner) error {
sklog.Info("Initializing Chromium builder service")
err := s.parseServiceArgs(serviceArgs)
if err != nil {
return err
}
sklog.Infof("Parsed args %v", s)
err = s.handleDepotToolsSetup(ctx, fs, cf, dc)
if err != nil {
return err
}
err = s.handleChromiumSetup(ctx, fs, cf, dc, ccr)
if err != nil {
return err
}
sklog.Info("Successfully initialized Chromium builder service")
return nil
}
// parseServiceArgs parses the string representation of the service's arguments
// and stores the resulting values in the ChromiumBuilderService.
func (s *ChromiumBuilderService) parseServiceArgs(serviceArgs string) error {
args := strings.Split(serviceArgs, ",")
for _, pair := range args {
splitPair := strings.SplitN(pair, "=", 2)
if len(splitPair) != 2 {
return skerr.Fmt("Argument %v is not in the expected key=value format", pair)
}
key := splitPair[0]
value := splitPair[1]
switch key {
case ArgDepotToolsPath:
s.depotToolsPath = value
case ArgChromiumPath:
s.chromiumPath = value
default:
return skerr.Fmt("Unknown argument key %v", key)
}
}
if s.depotToolsPath == "" {
return skerr.Fmt("Did not receive a %v argument", ArgDepotToolsPath)
}
if s.chromiumPath == "" {
return skerr.Fmt("Did not receive a %v argument", ArgChromiumPath)
}
return nil
}
// handleDepotTools ensures that a depot_tools checkout is available at the
// stored path.
func (s *ChromiumBuilderService) handleDepotToolsSetup(ctx context.Context, fs vfs.FS, cf checkoutFactory, dc directoryCreator) error {
// Check if depot_tools path exists.
depotToolsDir, err := fs.Open(ctx, s.depotToolsPath)
if err != nil {
if isNotExistWithUnwraps(err) {
return s.handleMissingDepotToolsCheckout(ctx, fs, cf, dc)
}
return err
}
defer depotToolsDir.Close(ctx)
return s.handleExistingDepotToolsCheckout(ctx, fs, cf)
}
// handleMissingDepotToolsCheckout sets up a new depot_tools checkout at the
// stored path to handle the case where there is not an existing checkout.
func (s *ChromiumBuilderService) handleMissingDepotToolsCheckout(ctx context.Context, fs vfs.FS, cf checkoutFactory, dc directoryCreator) error {
sklog.Infof("Did not find existing depot_tools checkout, cloning one at %s", s.depotToolsPath)
// Ensure the parent directories exist.
err := dc(filepath.Dir(s.depotToolsPath), 0o750)
if err != nil {
return err
}
// git.NewCheckout() clones the repo if a checkout doesn't exist at the
// given directory already, so rely on that behavior.
err = s.createDepotToolsCheckout(ctx, cf)
if err != nil {
return err
}
// Creating the checkout creates the repo, but doesn't fetch anything. So,
// perform an explicit update to pull everything down.
err = s.updateDepotToolsCheckout(ctx)
if err != nil {
return err
}
return nil
}
// handleExistingDepotToolsCheckout ensures that an existing depot_tools
// checkout is valid and up to date.
func (s *ChromiumBuilderService) handleExistingDepotToolsCheckout(ctx context.Context, fs vfs.FS, cf checkoutFactory) error {
sklog.Infof("Found existing depot_tools checkout at %s", s.depotToolsPath)
// Check that the provided path is actually a directory.
err := checkIfPathIsDirectory(ctx, fs, s.depotToolsPath)
if err != nil {
return err
}
// Check that an expected tool exists.
lucicfgPath := filepath.Join(s.depotToolsPath, "lucicfg")
lucicfg, err := fs.Open(ctx, lucicfgPath)
if err != nil {
return err
}
defer lucicfg.Close(ctx)
// Check that this appears to be an actual git repo.
dotGitPath := filepath.Join(s.depotToolsPath, ".git")
err = checkIfPathIsDirectory(ctx, fs, dotGitPath)
if err != nil {
return err
}
err = s.createDepotToolsCheckout(ctx, cf)
if err != nil {
return err
}
err = s.updateDepotToolsCheckout(ctx)
if err != nil {
return err
}
return nil
}
// handleChromiumSetup ensures that a Chromium checkout is available at the
// stored path.
func (s *ChromiumBuilderService) handleChromiumSetup(
ctx context.Context, fs vfs.FS, cf checkoutFactory, dc directoryCreator, ccr concurrentCommandRunner) error {
// Check if the Chromium path exists.
chromiumDir, err := fs.Open(ctx, s.chromiumPath)
if err != nil {
if isNotExistWithUnwraps(err) {
return s.handleMissingChromiumCheckout(ctx, fs, cf, dc, ccr)
}
return err
}
defer chromiumDir.Close(ctx)
return s.handleExistingChromiumCheckout(ctx, fs, cf)
}
// handleMissingChromiumCheckout sets up a new Chromium checkout at the stored
// path to handle the case where there is not an existing checkout.
func (s *ChromiumBuilderService) handleMissingChromiumCheckout(
ctx context.Context, fs vfs.FS, cf checkoutFactory, dc directoryCreator, ccr concurrentCommandRunner) error {
sklog.Infof("Did not find existing Chromium checkout, fetching one at %s", s.chromiumPath)
// Ensure the parent directories exist.
err := dc(filepath.Dir(s.chromiumPath), 0o750)
if err != nil {
return err
}
err = s.fetchChromium(ccr)
if err != nil {
return err
}
// Obtain a re-usable checkout.
err = s.createChromiumCheckout(ctx, cf)
if err != nil {
return err
}
// The checkout will already be up to date after the fetch, so no need to
// explicitly update here.
return nil
}
// handleExistingChromiumCheckout ensures that the existing Chromium checkout is
// valid and up to date.
func (s *ChromiumBuilderService) handleExistingChromiumCheckout(ctx context.Context, fs vfs.FS, cf checkoutFactory) error {
sklog.Infof("Found existing Chromium checkout at %s", s.chromiumPath)
// Check that the provided path is actually a directory.
err := checkIfPathIsDirectory(ctx, fs, s.chromiumPath)
if err != nil {
return err
}
// Check that this appears to be an actual git repo.
dotGitPath := filepath.Join(s.chromiumPath, ".git")
err = checkIfPathIsDirectory(ctx, fs, dotGitPath)
if err != nil {
return err
}
// Obtain a re-usable checkout and ensure it is up to date.
err = s.createChromiumCheckout(ctx, cf)
if err != nil {
return err
}
err = s.updateChromiumCheckout(ctx)
if err != nil {
return err
}
return nil
}
// fetchChromium fetches a Chromium checkout using the stored path.
func (s *ChromiumBuilderService) fetchChromium(ccr concurrentCommandRunner) error {
// If we end up cancelling the fetch command mid-run, we will have to
// perform additional cleanup in order to ensure that the checkout is not
// in a bad state. Hence, we have our own lock and cannot use
// runSafeCancellableCommand().
sklog.Infof("Fetching Chromium checkout into %s. This will take a while.", s.chromiumPath)
fetchPath := filepath.Join(s.depotToolsPath, "fetch")
output := bytes.Buffer{}
cmd := exec.Command{
Name: fetchPath,
Args: []string{"--nohooks", "chromium"},
CombinedOutput: &output,
Dir: filepath.Dir(s.chromiumPath),
}
err := s.runCancellableCommand(&cmd, ccr, &(s.chromiumFetchLock))
if err != nil {
return skerr.Fmt("Failed to fetch Chromium. Original error: %v Stdout: %s", err, output.String())
}
sklog.Info("Successfully fetched Chromium checkout")
return nil
}
// checkIfPathIsDirectory is a helper to check if the provided path exists and
// is a directory.
func checkIfPathIsDirectory(ctx context.Context, fs vfs.FS, path string) error {
// Check if the provided path exists.
fileHandle, err := fs.Open(ctx, path)
if err != nil {
return skerr.Wrap(err)
}
defer fileHandle.Close(ctx)
// Check if the provided path is actually a directory.
fileInfo, err := fileHandle.Stat(ctx)
if err != nil {
return skerr.Wrap(err)
}
if !fileInfo.IsDir() {
return skerr.Fmt("Path %s exists, but is not a directory.", path)
}
return nil
}
// isNotExistWithUnwraps is a helper function to run os.IsNotExist() on
// possibly wrapped errors.
func isNotExistWithUnwraps(err error) bool {
for err != nil {
if os.IsNotExist(err) {
return true
}
err = errors.Unwrap(err)
}
return false
}