blob: be99e56cb2e8689d3593a2a1cc7e63423a6699d4 [file]
// Package core contains the core Bazelisk logic, as well as abstractions for Bazel repositories.
package core
// TODO: split this file into multiple smaller ones in dedicated packages (e.g. execution, incompatible, ...).
import (
"bufio"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"syscall"
"github.com/bazelbuild/bazelisk/config"
"github.com/bazelbuild/bazelisk/httputil"
"github.com/bazelbuild/bazelisk/platforms"
"github.com/bazelbuild/bazelisk/versions"
"github.com/bazelbuild/bazelisk/ws"
"github.com/mitchellh/go-homedir"
)
const (
bazelReal = "BAZEL_REAL"
skipWrapperEnv = "BAZELISK_SKIP_WRAPPER"
defaultWrapperDirectory = "./tools"
defaultWrapperName = "bazel"
maxDirLength = 255
)
var (
// BazeliskVersion is filled in via x_defs when building a release.
BazeliskVersion = "development"
)
// ArgsFunc is a function that receives a resolved Bazel version and returns the arguments to invoke
// Bazel with.
type ArgsFunc func(resolvedBazelVersion string) []string
// MakeDefaultConfig returns a config based on env and .bazeliskrc files.
func MakeDefaultConfig() config.Config {
configs := []config.Config{config.FromEnv()}
workspaceConfigPath, err := config.LocateWorkspaceConfigFile()
if err == nil {
c, err := config.FromFile(workspaceConfigPath)
if err != nil {
log.Fatal(err)
}
configs = append(configs, c)
}
userConfigPath, err := config.LocateUserConfigFile()
if err == nil {
c, err := config.FromFile(userConfigPath)
if err != nil {
log.Fatal(err)
}
configs = append(configs, c)
}
return config.Layered(configs...)
}
// RunBazelisk runs the main Bazelisk logic for the given arguments and Bazel repositories.
func RunBazelisk(args []string, repos *Repositories) (int, error) {
return RunBazeliskWithArgsFunc(func(_ string) []string { return args }, repos)
}
// RunBazeliskWithArgsFunc runs the main Bazelisk logic for the given ArgsFunc and Bazel
// repositories.
func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error) {
return RunBazeliskWithArgsFuncAndConfig(argsFunc, repos, MakeDefaultConfig())
}
// RunBazeliskWithArgsFuncAndConfig runs the main Bazelisk logic for the given ArgsFunc and Bazel
// repositories and config.
func RunBazeliskWithArgsFuncAndConfig(argsFunc ArgsFunc, repos *Repositories, config config.Config) (int, error) {
return RunBazeliskWithArgsFuncAndConfigAndOut(argsFunc, repos, config, nil)
}
// RunBazeliskWithArgsFuncAndConfigAndOut runs the main Bazelisk logic for the given ArgsFunc and Bazel
// repositories and config, writing its stdout to the passed writer.
func RunBazeliskWithArgsFuncAndConfigAndOut(argsFunc ArgsFunc, repos *Repositories, config config.Config, out io.Writer) (int, error) {
httputil.UserAgent = getUserAgent(config)
bazeliskHome, err := getBazeliskHome(config)
if err != nil {
return -1, fmt.Errorf("could not determine Bazelisk home directory: %v", err)
}
err = os.MkdirAll(bazeliskHome, 0755)
if err != nil {
return -1, fmt.Errorf("could not create directory %s: %v", bazeliskHome, err)
}
bazelVersionString, err := GetBazelVersion(config)
if err != nil {
return -1, fmt.Errorf("could not get Bazel version: %v", err)
}
bazelPath, err := homedir.Expand(bazelVersionString)
if err != nil {
return -1, fmt.Errorf("could not expand home directory in path: %v", err)
}
// If the Bazel version is an absolute path to a Bazel binary in the filesystem, we can
// use it directly. In that case, we don't know which exact version it is, though.
resolvedBazelVersion := "unknown"
// If we aren't using a local Bazel binary, we'll have to parse the version string and
// download the version that the user wants.
if !filepath.IsAbs(bazelPath) {
bazelPath, err = downloadBazel(bazelVersionString, bazeliskHome, repos, config)
if err != nil {
return -1, fmt.Errorf("could not download Bazel: %v", err)
}
} else {
baseDirectory := filepath.Join(bazeliskHome, "local")
bazelPath, err = linkLocalBazel(baseDirectory, bazelPath)
if err != nil {
return -1, fmt.Errorf("could not link local Bazel: %v", err)
}
}
args := argsFunc(resolvedBazelVersion)
// --print_env must be the first argument.
if len(args) > 0 && args[0] == "--print_env" {
// print environment variables for sub-processes
cmd := makeBazelCmd(bazelPath, args, nil, config)
for _, val := range cmd.Env {
fmt.Println(val)
}
return 0, nil
}
// --strict and --migrate and --bisect must be the first argument.
if len(args) > 0 && (args[0] == "--strict" || args[0] == "--migrate") {
cmd, err := getBazelCommand(args)
if err != nil {
return -1, err
}
newFlags, err := getIncompatibleFlags(bazelPath, cmd, config)
if err != nil {
return -1, fmt.Errorf("could not get the list of incompatible flags: %v", err)
}
if args[0] == "--migrate" {
migrate(bazelPath, args[1:], newFlags, config)
} else {
// When --strict is present, it expands to the list of --incompatible_ flags
// that should be enabled for the given Bazel version.
args = insertArgs(args[1:], newFlags)
}
} else if len(args) > 0 && strings.HasPrefix(args[0], "--bisect") {
// When --bisect is present, we run the bisect logic.
if !strings.HasPrefix(args[0], "--bisect=") {
return -1, fmt.Errorf("Error: --bisect must have a value. Expected format: '--bisect=<good bazel commit>..<bad bazel commit>'")
}
value := args[0][len("--bisect="):]
commits := strings.Split(value, "..")
if len(commits) == 2 {
bisect(commits[0], commits[1], args[1:], bazeliskHome, repos, config)
} else {
return -1, fmt.Errorf("Error: Invalid format for --bisect. Expected format: '--bisect=<good bazel commit>..<bad bazel commit>'")
}
}
// print bazelisk version information if "version" is the first argument
// bazel version is executed after this command
if len(args) > 0 && args[0] == "version" {
// Check if the --gnu_format flag is set, if that is the case,
// the version is printed differently
var gnuFormat bool
for _, arg := range args {
if arg == "--gnu_format" {
gnuFormat = true
break
}
}
if gnuFormat {
fmt.Printf("Bazelisk %s\n", BazeliskVersion)
} else {
fmt.Printf("Bazelisk version: %s\n", BazeliskVersion)
}
}
exitCode, err := runBazel(bazelPath, args, out, config)
if err != nil {
return -1, fmt.Errorf("could not run Bazel: %v", err)
}
return exitCode, nil
}
func getBazelCommand(args []string) (string, error) {
for _, a := range args {
if !strings.HasPrefix(a, "-") {
return a, nil
}
}
return "", fmt.Errorf("could not find a valid Bazel command in %q. Please run `bazel help` if you need help on how to use Bazel", strings.Join(args, " "))
}
// getBazeliskHome returns the path to the Bazelisk home directory.
func getBazeliskHome(config config.Config) (string, error) {
bazeliskHome := config.Get("BAZELISK_HOME")
if len(bazeliskHome) == 0 {
userCacheDir, err := os.UserCacheDir()
if err != nil {
return "", fmt.Errorf("could not get the user's cache directory: %v", err)
}
bazeliskHome = filepath.Join(userCacheDir, "bazelisk")
} else {
// If a custom BAZELISK_HOME is set, handle tilde and var expansion
// before creating the Bazelisk home directory.
var err error
bazeliskHome, err = homedir.Expand(bazeliskHome)
if err != nil {
return "", fmt.Errorf("could not expand home directory in path: %v", err)
}
bazeliskHome = os.ExpandEnv(bazeliskHome)
}
return bazeliskHome, nil
}
func getUserAgent(config config.Config) string {
agent := config.Get("BAZELISK_USER_AGENT")
if len(agent) > 0 {
return agent
}
return fmt.Sprintf("Bazelisk/%s", BazeliskVersion)
}
// GetBazelVersion returns the Bazel version that should be used.
func GetBazelVersion(config config.Config) (string, error) {
// Check in this order:
// - env var "USE_BAZEL_VERSION" is set to a specific version.
// - workspace_root/.bazeliskrc exists -> read contents, in contents:
// var "USE_BAZEL_VERSION" is set to a specific version.
// - env var "USE_NIGHTLY_BAZEL" or "USE_BAZEL_NIGHTLY" is set -> latest
// nightly. (TODO)
// - env var "USE_CANARY_BAZEL" or "USE_BAZEL_CANARY" is set -> latest
// rc. (TODO)
// - the file workspace_root/tools/bazel exists -> that version. (TODO)
// - workspace_root/.bazelversion exists -> read contents, that version.
// - workspace_root/WORKSPACE contains a version -> that version. (TODO)
// - env var "USE_BAZEL_FALLBACK_VERSION" is set to a fallback version format.
// - workspace_root/.bazeliskrc exists -> read contents, in contents:
// var "USE_BAZEL_FALLBACK_VERSION" is set to a fallback version format.
// - fallback version format "silent:latest"
bazelVersion := config.Get("USE_BAZEL_VERSION")
if len(bazelVersion) != 0 {
return bazelVersion, nil
}
workingDirectory, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("could not get working directory: %v", err)
}
workspaceRoot := ws.FindWorkspaceRoot(workingDirectory)
if len(workspaceRoot) != 0 {
bazelVersionPath := filepath.Join(workspaceRoot, ".bazelversion")
if _, err := os.Stat(bazelVersionPath); err == nil {
f, err := os.Open(bazelVersionPath)
if err != nil {
return "", fmt.Errorf("could not read %s: %v", bazelVersionPath, err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Scan()
bazelVersion := scanner.Text()
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("could not read version from file %s: %v", bazelVersion, err)
}
if len(bazelVersion) != 0 {
return bazelVersion, nil
}
}
}
fallbackVersionFormat := config.Get("USE_BAZEL_FALLBACK_VERSION")
fallbackVersionMode, fallbackVersion, hasFallbackVersionMode := strings.Cut(fallbackVersionFormat, ":")
if !hasFallbackVersionMode {
fallbackVersionMode, fallbackVersion, hasFallbackVersionMode = "silent", fallbackVersionMode, true
}
if len(fallbackVersion) == 0 {
fallbackVersion = "latest"
}
if fallbackVersionMode == "error" {
return "", fmt.Errorf("not allowed to use fallback version %q", fallbackVersion)
}
if fallbackVersionMode == "warn" {
log.Printf("Warning: used fallback version %q\n", fallbackVersion)
return fallbackVersion, nil
}
if fallbackVersionMode == "silent" {
return fallbackVersion, nil
}
return "", fmt.Errorf("invalid fallback version format %q (effectively %q)", fallbackVersionFormat, fmt.Sprintf("%s:%s", fallbackVersionMode, fallbackVersion))
}
func parseBazelForkAndVersion(bazelForkAndVersion string) (string, string, error) {
var bazelFork, bazelVersion string
versionInfo := strings.Split(bazelForkAndVersion, "/")
if len(versionInfo) == 1 {
bazelFork, bazelVersion = versions.BazelUpstream, versionInfo[0]
} else if len(versionInfo) == 2 {
bazelFork, bazelVersion = versionInfo[0], versionInfo[1]
} else {
return "", "", fmt.Errorf("invalid version \"%s\", could not parse version with more than one slash", bazelForkAndVersion)
}
return bazelFork, bazelVersion, nil
}
func downloadBazel(bazelVersionString string, bazeliskHome string, repos *Repositories, config config.Config) (string, error) {
bazelFork, bazelVersion, err := parseBazelForkAndVersion(bazelVersionString)
if err != nil {
return "", fmt.Errorf("could not parse Bazel fork and version: %v", err)
}
resolvedBazelVersion, downloader, err := repos.ResolveVersion(bazeliskHome, bazelFork, bazelVersion, config)
if err != nil {
return "", fmt.Errorf("could not resolve the version '%s' to an actual version number: %v", bazelVersion, err)
}
bazelForkOrURL := dirForURL(config.Get(BaseURLEnv))
if len(bazelForkOrURL) == 0 {
bazelForkOrURL = bazelFork
}
bazelPath, err := downloadBazelIfNecessary(resolvedBazelVersion, bazeliskHome, bazelForkOrURL, repos, config, downloader)
return bazelPath, err
}
// downloadBazelIfNecessary returns a path to a bazel which can be run, which may have been cached.
// The directory it returns may depend on version and bazeliskHome, but does not depend on bazelForkOrURLDirName.
// This is important, as the directory may be added to $PATH, and varying the path for equivalent files may cause unnecessary repository rule cache invalidations.
// Where a file was downloaded from shouldn't affect cache behaviour of Bazel invocations.
//
// The structure of the downloads directory is as follows ([]s indicate variables):
//
// downloads/metadata/[fork-or-url]/bazel-[version-os-etc] is a text file containing a hex sha256 of the contents of the downloaded bazel file.
// downloads/sha256/[sha256]/bin/bazel[extension] contains the bazel with a particular sha256.
func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrURLDirName string, repos *Repositories, config config.Config, downloader DownloadFunc) (string, error) {
pathSegment, err := platforms.DetermineBazelFilename(version, false, config)
if err != nil {
return "", fmt.Errorf("could not determine path segment to use for Bazel binary: %v", err)
}
destFile := "bazel" + platforms.DetermineExecutableFilenameSuffix()
mappingPath := filepath.Join(bazeliskHome, "downloads", "metadata", bazelForkOrURLDirName, pathSegment)
digestFromMappingFile, err := os.ReadFile(mappingPath)
if err == nil {
pathToBazelInCAS := filepath.Join(bazeliskHome, "downloads", "sha256", string(digestFromMappingFile), "bin", destFile)
if _, err := os.Stat(pathToBazelInCAS); err == nil {
return pathToBazelInCAS, nil
}
}
pathToBazelInCAS, downloadedDigest, err := downloadBazelToCAS(version, bazeliskHome, repos, config, downloader)
if err != nil {
return "", fmt.Errorf("failed to download bazel: %w", err)
}
expectedSha256 := strings.ToLower(config.Get("BAZELISK_VERIFY_SHA256"))
if len(expectedSha256) > 0 {
if expectedSha256 != downloadedDigest {
return "", fmt.Errorf("%s has sha256=%s but need sha256=%s", pathToBazelInCAS, downloadedDigest, expectedSha256)
}
}
if err := atomicWriteFile(mappingPath, []byte(downloadedDigest), 0644); err != nil {
return "", fmt.Errorf("failed to write mapping file after downloading bazel: %w", err)
}
return pathToBazelInCAS, nil
}
func atomicWriteFile(path string, contents []byte, perm os.FileMode) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("failed to MkdirAll parent of %s: %w", path, err)
}
tmpPath := path + ".tmp"
if err := os.WriteFile(tmpPath, contents, perm); err != nil {
return fmt.Errorf("failed to write file %s: %w", tmpPath, err)
}
if err := os.Rename(tmpPath, path); err != nil {
return fmt.Errorf("failed to rename %s to %s: %w", tmpPath, path, err)
}
return nil
}
func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories, config config.Config, downloader DownloadFunc) (string, string, error) {
downloadsDir := filepath.Join(bazeliskHome, "downloads")
temporaryDownloadDir := filepath.Join(downloadsDir, "_tmp")
casDir := filepath.Join(bazeliskHome, "downloads", "sha256")
tmpDestFileBytes := make([]byte, 32)
if _, err := rand.Read(tmpDestFileBytes); err != nil {
return "", "", fmt.Errorf("failed to generate temporary file name: %w", err)
}
tmpDestFile := fmt.Sprintf("%x", tmpDestFileBytes)
var tmpDestPath string
var err error
baseURL := config.Get(BaseURLEnv)
formatURL := config.Get(FormatURLEnv)
if baseURL != "" && formatURL != "" {
return "", "", fmt.Errorf("cannot set %s and %s at once", BaseURLEnv, FormatURLEnv)
} else if formatURL != "" {
tmpDestPath, err = repos.DownloadFromFormatURL(config, formatURL, version, temporaryDownloadDir, tmpDestFile)
} else if baseURL != "" {
tmpDestPath, err = repos.DownloadFromBaseURL(baseURL, version, temporaryDownloadDir, tmpDestFile, config)
} else {
tmpDestPath, err = downloader(temporaryDownloadDir, tmpDestFile)
}
if err != nil {
return "", "", fmt.Errorf("failed to download bazel: %w", err)
}
f, err := os.Open(tmpDestPath)
if err != nil {
return "", "", fmt.Errorf("failed to open downloaded bazel to digest it: %w", err)
}
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
f.Close()
return "", "", fmt.Errorf("cannot compute sha256 of %s after download: %v", tmpDestPath, err)
}
f.Close()
actualSha256 := strings.ToLower(fmt.Sprintf("%x", h.Sum(nil)))
pathToBazelInCAS := filepath.Join(casDir, actualSha256, "bin", "bazel"+platforms.DetermineExecutableFilenameSuffix())
if err := os.MkdirAll(filepath.Dir(pathToBazelInCAS), 0755); err != nil {
return "", "", fmt.Errorf("failed to MkdirAll parent of %s: %w", pathToBazelInCAS, err)
}
tmpPathInCorrectDirectory := pathToBazelInCAS + ".tmp"
if err := os.Rename(tmpDestPath, tmpPathInCorrectDirectory); err != nil {
return "", "", fmt.Errorf("failed to move %s to %s: %w", tmpDestPath, tmpPathInCorrectDirectory, err)
}
if err := os.Rename(tmpPathInCorrectDirectory, pathToBazelInCAS); err != nil {
return "", "", fmt.Errorf("failed to move %s to %s: %w", tmpPathInCorrectDirectory, pathToBazelInCAS, err)
}
return pathToBazelInCAS, actualSha256, nil
}
func copyFile(src, dst string, perm os.FileMode) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, perm)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
}
func linkLocalBazel(baseDirectory string, bazelPath string) (string, error) {
normalizedBazelPath := dirForURL(bazelPath)
destinationDir := filepath.Join(baseDirectory, normalizedBazelPath, "bin")
err := os.MkdirAll(destinationDir, 0755)
if err != nil {
return "", fmt.Errorf("could not create directory %s: %v", destinationDir, err)
}
destinationPath := filepath.Join(destinationDir, "bazel"+platforms.DetermineExecutableFilenameSuffix())
if _, err := os.Stat(destinationPath); err != nil {
err = os.Symlink(bazelPath, destinationPath)
// If can't create Symlink, fallback to copy
if err != nil {
err = copyFile(bazelPath, destinationPath, 0755)
if err != nil {
return "", fmt.Errorf("could not copy file from %s to %s: %v", bazelPath, destinationPath, err)
}
}
}
return destinationPath, nil
}
func maybeDelegateToWrapperFromDir(bazel string, wd string, config config.Config) string {
if config.Get(skipWrapperEnv) != "" {
return bazel
}
wrapperPath := config.Get("BAZELISK_WRAPPER_DIRECTORY")
if wrapperPath == "" {
wrapperPath = filepath.Join(defaultWrapperDirectory, defaultWrapperName)
} else {
wrapperPath = filepath.Join(wrapperPath, defaultWrapperName)
}
root := ws.FindWorkspaceRoot(wd)
wrapper := filepath.Join(root, wrapperPath)
if stat, err := os.Stat(wrapper); err == nil && !stat.Mode().IsDir() && stat.Mode().Perm()&0111 != 0 {
return wrapper
}
if runtime.GOOS == "windows" {
powershellWrapper := filepath.Join(root, wrapperPath+".ps1")
if stat, err := os.Stat(powershellWrapper); err == nil && !stat.Mode().IsDir() {
return powershellWrapper
}
batchWrapper := filepath.Join(root, wrapperPath+".bat")
if stat, err := os.Stat(batchWrapper); err == nil && !stat.Mode().IsDir() {
return batchWrapper
}
}
return bazel
}
func maybeDelegateToWrapper(bazel string, config config.Config) string {
wd, err := os.Getwd()
if err != nil {
return bazel
}
return maybeDelegateToWrapperFromDir(bazel, wd, config)
}
func prependDirToPathList(cmd *exec.Cmd, dir string) {
found := false
for idx, val := range cmd.Env {
splits := strings.Split(val, "=")
if len(splits) != 2 {
continue
}
if strings.EqualFold(splits[0], "PATH") {
found = true
cmd.Env[idx] = fmt.Sprintf("PATH=%s%s%s", dir, string(os.PathListSeparator), splits[1])
break
}
}
if !found {
cmd.Env = append(cmd.Env, fmt.Sprintf("PATH=%s", dir))
}
}
func makeBazelCmd(bazel string, args []string, out io.Writer, config config.Config) *exec.Cmd {
execPath := maybeDelegateToWrapper(bazel, config)
cmd := exec.Command(execPath, args...)
cmd.Env = append(os.Environ(), skipWrapperEnv+"=true")
if execPath != bazel {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", bazelReal, bazel))
}
prependDirToPathList(cmd, filepath.Dir(execPath))
cmd.Stdin = os.Stdin
if out == nil {
cmd.Stdout = os.Stdout
} else {
cmd.Stdout = out
}
cmd.Stderr = os.Stderr
return cmd
}
func runBazel(bazel string, args []string, out io.Writer, config config.Config) (int, error) {
cmd := makeBazelCmd(bazel, args, out, config)
err := cmd.Start()
if err != nil {
return 1, fmt.Errorf("could not start Bazel: %v", err)
}
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
s := <-c
// Only forward SIGTERM to our child process.
if s != os.Interrupt {
cmd.Process.Kill()
}
}()
err = cmd.Wait()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
waitStatus := exitError.Sys().(syscall.WaitStatus)
return waitStatus.ExitStatus(), nil
}
return 1, fmt.Errorf("could not launch Bazel: %v", err)
}
return 0, nil
}
// getIncompatibleFlags returns all incompatible flags for the current Bazel command in alphabetical order.
func getIncompatibleFlags(bazelPath, cmd string, config config.Config) ([]string, error) {
var incompatibleFlagsStr = config.Get("BAZELISK_INCOMPATIBLE_FLAGS")
if len(incompatibleFlagsStr) > 0 {
return strings.Split(incompatibleFlagsStr, ","), nil
}
out := strings.Builder{}
if _, err := runBazel(bazelPath, []string{"help", cmd, "--short"}, &out, config); err != nil {
return nil, fmt.Errorf("unable to determine incompatible flags with binary %s: %v", bazelPath, err)
}
re := regexp.MustCompile(`(?m)^\s*--\[no\](incompatible_\w+)$`)
flags := make([]string, 0)
for _, m := range re.FindAllStringSubmatch(out.String(), -1) {
flags = append(flags, fmt.Sprintf("--%s", m[1]))
}
sort.Strings(flags)
return flags, nil
}
// insertArgs will insert newArgs in baseArgs. If baseArgs contains the
// "--" argument, newArgs will be inserted before that. Otherwise, newArgs
// is appended.
func insertArgs(baseArgs []string, newArgs []string) []string {
var result []string
inserted := false
for _, arg := range baseArgs {
if !inserted && arg == "--" {
result = append(result, newArgs...)
inserted = true
}
result = append(result, arg)
}
if !inserted {
result = append(result, newArgs...)
}
return result
}
func parseStartupOptions(baseArgs []string) []string {
var result []string
var bazelCommands = map[string]bool{
"analyze-profile": true,
"aquery": true,
"build": true,
"canonicalize-flags": true,
"clean": true,
"coverage": true,
"cquery": true,
"dump": true,
"fetch": true,
"help": true,
"info": true,
"license": true,
"mobile-install": true,
"mod": true,
"print_action": true,
"query": true,
"run": true,
"shutdown": true,
"sync": true,
"test": true,
"version": true,
}
// Arguments before a Bazel command are startup options.
for _, arg := range baseArgs {
if _, ok := bazelCommands[arg]; ok {
return result
}
result = append(result, arg)
}
return result
}
func shutdownIfNeeded(bazelPath string, startupOptions []string, config config.Config) {
bazeliskClean := config.Get("BAZELISK_SHUTDOWN")
if len(bazeliskClean) == 0 {
return
}
args := append(startupOptions, "shutdown")
fmt.Printf("bazel %s\n", strings.Join(args, " "))
exitCode, err := runBazel(bazelPath, args, nil, config)
fmt.Printf("\n")
if err != nil {
log.Fatalf("failed to run bazel shutdown: %v", err)
}
if exitCode != 0 {
fmt.Printf("Failure: shutdown command failed.\n")
os.Exit(exitCode)
}
}
func cleanIfNeeded(bazelPath string, startupOptions []string, config config.Config) {
bazeliskClean := config.Get("BAZELISK_CLEAN")
if len(bazeliskClean) == 0 {
return
}
args := append(startupOptions, "clean", "--expunge")
fmt.Printf("bazel %s\n", strings.Join(args, " "))
exitCode, err := runBazel(bazelPath, args, nil, config)
fmt.Printf("\n")
if err != nil {
log.Fatalf("failed to run clean: %v", err)
}
if exitCode != 0 {
fmt.Printf("Failure: clean command failed.\n")
os.Exit(exitCode)
}
}
type parentCommit struct {
SHA string `json:"sha"`
}
type commit struct {
SHA string `json:"sha"`
PARENTS []parentCommit `json:"parents"`
}
type compareResponse struct {
Commits []commit `json:"commits"`
BaseCommit commit `json:"base_commit"`
MergeBaseCommit commit `json:"merge_base_commit"`
}
func sendRequest(url string, config config.Config) (*http.Response, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
githubToken := config.Get("BAZELISK_GITHUB_TOKEN")
if len(githubToken) != 0 {
req.Header.Set("Authorization", fmt.Sprintf("token %s", githubToken))
}
return client.Do(req)
}
func getBazelCommitsBetween(goodCommit string, badCommit string, config config.Config) (string, []string, error) {
commitList := make([]string, 0)
page := 1
perPage := 250 // 250 is the maximum number of commits per page
for {
url := fmt.Sprintf("https://api.github.com/repos/bazelbuild/bazel/compare/%s...%s?page=%d&per_page=%d", goodCommit, badCommit, page, perPage)
response, err := sendRequest(url, config)
if err != nil {
return goodCommit, nil, fmt.Errorf("Error fetching commit data: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return goodCommit, nil, fmt.Errorf("Error reading response body: %v", err)
}
if response.StatusCode == http.StatusNotFound {
return goodCommit, nil, fmt.Errorf("repository or commit not found: %s", string(body))
} else if response.StatusCode == 403 {
return goodCommit, nil, fmt.Errorf("github API rate limit hit, consider setting BAZELISK_GITHUB_TOKEN: %s", string(body))
} else if response.StatusCode != http.StatusOK {
return goodCommit, nil, fmt.Errorf("unexpected response status code %d: %s", response.StatusCode, string(body))
}
var compResp compareResponse
err = json.Unmarshal(body, &compResp)
if err != nil {
return goodCommit, nil, fmt.Errorf("Error unmarshaling JSON: %v", err)
}
if len(compResp.Commits) == 0 {
break
}
mergeBaseCommit := compResp.MergeBaseCommit.SHA
if mergeBaseCommit != compResp.BaseCommit.SHA {
fmt.Printf("The good Bazel commit is not an ancestor of the bad Bazel commit, overriding the good Bazel commit to the merge base commit %s\n", mergeBaseCommit)
goodCommit = mergeBaseCommit
}
for _, commit := range compResp.Commits {
// If it has only one parent commit, add it to the list, otherwise it's a merge commit and we ignore it
if len(commit.PARENTS) == 1 {
commitList = append(commitList, commit.SHA)
}
}
// Check if there are more commits to fetch
if len(compResp.Commits) < perPage {
break
}
page++
}
if len(commitList) == 0 {
return goodCommit, nil, fmt.Errorf("no commits found between (%s, %s], the good commit should be first, maybe try with --bisect=%s..%s ?", goodCommit, badCommit, badCommit, goodCommit)
}
fmt.Printf("Found %d commits between (%s, %s]\n", len(commitList), goodCommit, badCommit)
return goodCommit, commitList, nil
}
func bisect(goodCommit string, badCommit string, args []string, bazeliskHome string, repos *Repositories, config config.Config) {
// 1. Get the list of commits between goodCommit and badCommit
fmt.Printf("\n\n--- Getting the list of commits between %s and %s\n\n", goodCommit, badCommit)
goodCommit, commitList, err := getBazelCommitsBetween(goodCommit, badCommit, config)
if err != nil {
log.Fatalf("Failed to get commits: %v", err)
os.Exit(1)
}
// 2. Check if goodCommit is actually good
fmt.Printf("\n\n--- Verifying if the given good Bazel commit (%s) is actually good\n\n", goodCommit)
bazelExitCode, err := testWithBazelAtCommit(goodCommit, args, bazeliskHome, repos, config)
if err != nil {
log.Fatalf("could not run Bazel: %v", err)
os.Exit(1)
}
if bazelExitCode != 0 {
fmt.Printf("Failure: Given good bazel commit is already broken.\n")
os.Exit(1)
}
// 3. Bisect commits
fmt.Printf("\n\n--- Start bisecting\n\n")
left := 0
right := len(commitList)
for left < right {
mid := (left + right) / 2
midCommit := commitList[mid]
fmt.Printf("\n\n--- Testing with Bazel built at %s, %d commits remaining...\n\n", midCommit, right-left)
bazelExitCode, err := testWithBazelAtCommit(midCommit, args, bazeliskHome, repos, config)
if err != nil {
log.Fatalf("could not run Bazel: %v", err)
os.Exit(1)
}
if bazelExitCode == 0 {
fmt.Printf("\n\n--- Succeeded at %s\n\n", midCommit)
left = mid + 1
} else {
fmt.Printf("\n\n--- Failed at %s\n\n", midCommit)
right = mid
}
}
// 4. Print the result
fmt.Printf("\n\n--- Bisect Result\n\n")
if right == len(commitList) {
fmt.Printf("first bad commit not found, every commit succeeded.\n")
} else {
firstBadCommit := commitList[right]
fmt.Printf("first bad commit is https://github.com/bazelbuild/bazel/commit/%s\n", firstBadCommit)
}
os.Exit(0)
}
func testWithBazelAtCommit(bazelCommit string, args []string, bazeliskHome string, repos *Repositories, config config.Config) (int, error) {
bazelPath, err := downloadBazel(bazelCommit, bazeliskHome, repos, config)
if err != nil {
return 1, fmt.Errorf("could not download Bazel: %v", err)
}
startupOptions := parseStartupOptions(args)
shutdownIfNeeded(bazelPath, startupOptions, config)
cleanIfNeeded(bazelPath, startupOptions, config)
fmt.Printf("bazel %s\n", strings.Join(args, " "))
bazelExitCode, err := runBazel(bazelPath, args, nil, config)
if err != nil {
return -1, fmt.Errorf("could not run Bazel: %v", err)
}
return bazelExitCode, nil
}
// migrate will run Bazel with each flag separately and report which ones are failing.
func migrate(bazelPath string, baseArgs []string, flags []string, config config.Config) {
var startupOptions = parseStartupOptions(baseArgs)
// 1. Try with all the flags.
args := insertArgs(baseArgs, flags)
fmt.Printf("\n\n--- Running Bazel with all incompatible flags\n\n")
shutdownIfNeeded(bazelPath, startupOptions, config)
cleanIfNeeded(bazelPath, startupOptions, config)
fmt.Printf("bazel %s\n", strings.Join(args, " "))
exitCode, err := runBazel(bazelPath, args, nil, config)
if err != nil {
log.Fatalf("could not run Bazel: %v", err)
}
if exitCode == 0 {
fmt.Printf("Success: No migration needed.\n")
os.Exit(0)
}
// 2. Try with no flags, as a sanity check.
args = baseArgs
fmt.Printf("\n\n--- Running Bazel with no incompatible flags\n\n")
shutdownIfNeeded(bazelPath, startupOptions, config)
cleanIfNeeded(bazelPath, startupOptions, config)
fmt.Printf("bazel %s\n", strings.Join(args, " "))
exitCode, err = runBazel(bazelPath, args, nil, config)
if err != nil {
log.Fatalf("could not run Bazel: %v", err)
}
if exitCode != 0 {
fmt.Printf("Failure: Command failed, even without incompatible flags.\n")
os.Exit(exitCode)
}
// 3. Try with each flag separately.
var passList []string
var failList []string
for _, arg := range flags {
args = insertArgs(baseArgs, []string{arg})
fmt.Printf("\n\n--- Running Bazel with %s\n\n", arg)
shutdownIfNeeded(bazelPath, startupOptions, config)
cleanIfNeeded(bazelPath, startupOptions, config)
fmt.Printf("bazel %s\n", strings.Join(args, " "))
exitCode, err = runBazel(bazelPath, args, nil, config)
if err != nil {
log.Fatalf("could not run Bazel: %v", err)
}
if exitCode == 0 {
passList = append(passList, arg)
} else {
failList = append(failList, arg)
}
}
print := func(l []string) {
for _, arg := range l {
fmt.Printf(" %s\n", arg)
}
}
// 4. Print report
fmt.Printf("\n\n+++ Result\n\n")
fmt.Printf("Command was successful with the following flags:\n")
print(passList)
fmt.Printf("\n")
fmt.Printf("Migration is needed for the following flags:\n")
print(failList)
os.Exit(1)
}
func dirForURL(url string) string {
// Replace all characters that might not be allowed in filenames with "-".
dir := regexp.MustCompile("[[:^alnum:]]").ReplaceAllString(url, "-")
// Work around length limit on some systems by truncating and then appending
// a sha256 hash of the URL.
if len(dir) > maxDirLength {
suffix := fmt.Sprintf("...%x", sha256.Sum256([]byte(url)))
dir = dir[:maxDirLength-len(suffix)] + suffix
}
return dir
}