blob: acf8f83e5df179b119722ee27d01c63824f4d564 [file] [log] [blame]
package bazel
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/task_driver/go/lib/os_steps"
"go.skia.org/infra/task_driver/go/td"
)
// Bazel provides a Task Driver API for working with Bazel (via the Bazelisk launcher, see
// https://github.com/bazelbuild/bazelisk).
type Bazel struct {
rbeCredentialFile string
workspace string
}
// NewWithRamdisk returns a new Bazel instance which uses a ramdisk as the Bazel cache.
//
// Using a ramdisk as the Bazel cache prevents CockroachDB "disk stall detected" errors on GCE VMs
// due to slow I/O.
func NewWithRamdisk(ctx context.Context, workspace string, rbeCredentialFile string, sizeGb int) (*Bazel, func(), error) {
// Create and mount ramdisk.
//
// At the time of writing, a full build of the Buildbot repository on an empty Bazel cache takes
// ~20GB on cache space. Infra-PerCommit-Test-Bazel-Local runs on GCE VMs with 64GB of RAM.
ramdiskDir, err := os_steps.TempDir(ctx, "", "ramdisk_*")
if err != nil {
return nil, nil, skerr.Wrap(err)
}
if _, err := exec.RunCwd(ctx, workspace, "sudo", "mount", "-t", "tmpfs", "-o", fmt.Sprintf("size=%dg", sizeGb), "tmpfs", ramdiskDir); err != nil {
return nil, nil, skerr.Wrap(err)
}
// Create Bazel cache directory inside the ramdisk.
//
// Using the ramdisk's mount point directly as the Bazel cache causes Bazel to fail with a file
// permission error. Using a directory within the ramdisk as the Bazel cache prevents this error.
cachePath := filepath.Join(ramdiskDir, "bazel_cache")
if err := os_steps.MkdirAll(ctx, cachePath); err != nil {
return nil, nil, skerr.Wrap(err)
}
repositoryCachePath := filepath.Join(ramdiskDir, "bazel_repo_cache")
if err := os_steps.MkdirAll(ctx, repositoryCachePath); err != nil {
return nil, nil, skerr.Wrap(err)
}
opts := BazelOptions{
CachePath: cachePath,
RepositoryCachePath: repositoryCachePath,
}
if err := EnsureBazelRCFile(ctx, opts); err != nil {
return nil, nil, skerr.Wrap(err)
}
absCredentialFile, err := os_steps.Abs(ctx, rbeCredentialFile)
if err != nil {
return nil, nil, skerr.Wrap(err)
}
bzl := &Bazel{
rbeCredentialFile: absCredentialFile,
workspace: workspace,
}
cleanup := func() {
// Shut down the Bazel server. This ensures that there are no processes with open files under
// the ramdisk, which would otherwise cause a "target is busy" when we unmount the ramdisk.
if _, err := bzl.Do(ctx, "shutdown"); err != nil {
td.Fatal(ctx, err)
}
if _, err := exec.RunCwd(ctx, workspace, "sudo", "umount", ramdiskDir); err != nil {
td.Fatal(ctx, err)
}
if err := os_steps.RemoveAll(ctx, ramdiskDir); err != nil {
td.Fatal(ctx, err)
}
}
return bzl, cleanup, nil
}
type BazelOptions struct {
CachePath string
RepositoryCachePath string
}
// EnsureBazelRCFile makes sure the user .bazelrc file exists and matches the provided
// configuration. This makes it easy for all subsequent calls to Bazel use the right command
// line args, even if Bazel is not invoked directly from task_driver (e.g. from a Makefile).
func EnsureBazelRCFile(ctx context.Context, bazelOpts BazelOptions) error {
var bazelRCLines []string
if bazelOpts.CachePath != "" {
// https://docs.bazel.build/versions/main/output_directories.html#current-layout
bazelRCLines = append(bazelRCLines, fmt.Sprintf("startup --output_user_root=%s", bazelOpts.CachePath))
}
if bazelOpts.RepositoryCachePath != "" {
// Also keep the repository cache on disk so that it survives reboots.
// https://bazel.build/docs/build#repository-cache
bazelRCLines = append(bazelRCLines, fmt.Sprintf("build --repository_cache=%s", bazelOpts.RepositoryCachePath))
}
// https://docs.bazel.build/versions/main/guide.html#where-are-the-bazelrc-files
// We go for the user's .bazelrc file instead of the system one because the
// swarming user does not have access to write to /etc/bazel.bazelrc
homeDir, err := os.UserHomeDir()
if err != nil {
return skerr.Wrap(err)
}
userBazelRCLocation := filepath.Join(homeDir, ".bazelrc")
bazelRCContent := strings.Join(bazelRCLines, "\n")
return os_steps.WriteFile(ctx, userBazelRCLocation, []byte(bazelRCContent), 0666)
}
// New returns a new Bazel instance.
func New(ctx context.Context, workspace, rbeCredentialFile string, opts BazelOptions) (*Bazel, error) {
if err := EnsureBazelRCFile(ctx, opts); err != nil {
return nil, skerr.Wrap(err)
}
absCredentialFile := ""
var err error
if rbeCredentialFile != "" {
absCredentialFile, err = os_steps.Abs(ctx, rbeCredentialFile)
if err != nil {
return nil, skerr.Wrap(err)
}
}
return &Bazel{
rbeCredentialFile: absCredentialFile,
workspace: workspace,
}, nil
}
// Do executes a Bazel subcommand.
func (b *Bazel) Do(ctx context.Context, subCmd string, args ...string) (string, error) {
cmd := []string{"bazelisk", subCmd}
cmd = append(cmd, args...)
return exec.RunCwd(ctx, b.workspace, cmd...)
}
// DoOnRBE executes a Bazel subcommand on RBE.
func (b *Bazel) DoOnRBE(ctx context.Context, subCmd string, args ...string) (string, error) {
// See https://bazel.build/reference/command-line-reference
cmd := []string{
"--config=remote",
"--sandbox_base=/dev/shm", // Make builds faster by using a RAM disk for the sandbox.
}
if b.rbeCredentialFile != "" {
cmd = append(cmd, "--google_credentials="+b.rbeCredentialFile)
} else {
cmd = append(cmd, "--google_default_credentials")
}
cmd = append(cmd, args...)
return b.Do(ctx, subCmd, cmd...)
}