diff --git a/bazelisk.go b/bazelisk.go index 5cdee070..a25041c1 100644 --- a/bazelisk.go +++ b/bazelisk.go @@ -25,11 +25,12 @@ import ( func main() { gcs := &repositories.GCSRepo{} - gitHub := repositories.CreateGitHubRepo(core.GetEnvOrConfig("BAZELISK_GITHUB_TOKEN")) + config := core.MakeDefaultConfig() + gitHub := repositories.CreateGitHubRepo(config.Get("BAZELISK_GITHUB_TOKEN")) // Fetch LTS releases, release candidates, rolling releases and Bazel-at-commits from GCS, forks from GitHub. repos := core.CreateRepositories(gcs, gcs, gitHub, gcs, gcs, true) - exitCode, err := core.RunBazelisk(os.Args[1:], repos) + exitCode, err := core.RunBazeliskWithArgsFuncAndConfig(func(string) []string { return os.Args[1:] }, repos, config) if err != nil { log.Fatal(err) } diff --git a/config/BUILD b/config/BUILD new file mode 100644 index 00000000..4752410e --- /dev/null +++ b/config/BUILD @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["config.go"], + importpath = "github.com/bazelbuild/bazelisk/config", + visibility = ["//visibility:public"], + deps = [ + "//ws:go_default_library", + "@com_github_mitchellh_go_homedir//:go_default_library", + ], +) diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..25afbe4e --- /dev/null +++ b/config/config.go @@ -0,0 +1,130 @@ +package config + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/bazelbuild/bazelisk/ws" +) + +const rcFileName = ".bazeliskrc" + +// Config allows getting Bazelisk configuration values. +type Config interface { + Get(name string) string +} + +// FromEnv returns a Config which gets config values from environment variables. +func FromEnv() Config { + return &fromEnv{} +} + +type fromEnv struct{} + +func (c *fromEnv) Get(name string) string { + return os.Getenv(name) +} + +// FromFile returns a Config which gets config values from a Bazelisk config file. +func FromFile(path string) (Config, error) { + values, err := parseFileConfig(path) + if err != nil { + return nil, err + } + return &static{ + values: values, + }, nil +} + +type static struct { + values map[string]string +} + +func (c *static) Get(name string) string { + return c.values[name] +} + +// parseFileConfig parses a .bazeliskrc file as a map of key-value configuration values. +func parseFileConfig(rcFilePath string) (map[string]string, error) { + config := make(map[string]string) + + contents, err := ioutil.ReadFile(rcFilePath) + if err != nil { + if os.IsNotExist(err) { + // Non-critical error. + return config, nil + } + return nil, err + } + + for _, line := range strings.Split(string(contents), "\n") { + if strings.HasPrefix(line, "#") { + // comments + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) < 2 { + continue + } + key := strings.TrimSpace(parts[0]) + config[key] = strings.TrimSpace(parts[1]) + } + + return config, nil +} + +// LocateUserConfigFile locates a .bazeliskrc file in the user's home directory. +func LocateUserConfigFile() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, rcFileName), nil +} + +// LocateWorkspaceConfigFile locates a .bazeliskrc file in the current workspace root. +func LocateWorkspaceConfigFile() (string, error) { + workingDirectory, err := os.Getwd() + if err != nil { + return "", err + } + workspaceRoot := ws.FindWorkspaceRoot(workingDirectory) + if workspaceRoot == "" { + return "", err + } + return filepath.Join(workspaceRoot, rcFileName), nil +} + +// Layered returns a Config which gets config values from the first of a series of other Config values which sets the config. +func Layered(configs ...Config) Config { + return &layered{ + configs: configs, + } +} + +type layered struct { + configs []Config +} + +func (c *layered) Get(name string) string { + for _, config := range c.configs { + if value := config.Get(name); value != "" { + return value + } + } + return "" +} + +// Null returns a Config with no config values. +func Null() Config { + return &static{} +} + +// Static returns a Config with static values. +func Static(values map[string]string) Config { + return &static{ + values: values, + } +} diff --git a/core/BUILD b/core/BUILD index b941e3d2..14585169 100644 --- a/core/BUILD +++ b/core/BUILD @@ -10,9 +10,11 @@ go_library( visibility = ["//visibility:public"], x_defs = {"BazeliskVersion": "{STABLE_VERSION}"}, deps = [ + "//config:go_default_library", "//httputil:go_default_library", "//platforms:go_default_library", "//versions:go_default_library", + "//ws:go_default_library", "@com_github_mitchellh_go_homedir//:go_default_library", ], ) @@ -24,4 +26,7 @@ go_test( "repositories_test.go", ], embed = [":go_default_library"], + deps = [ + "//config:go_default_library", + ], ) diff --git a/core/core.go b/core/core.go index f08a3e66..cab98d9b 100644 --- a/core/core.go +++ b/core/core.go @@ -24,9 +24,11 @@ import ( "sync" "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" ) @@ -34,7 +36,6 @@ const ( bazelReal = "BAZEL_REAL" skipWrapperEnv = "BAZELISK_SKIP_WRAPPER" wrapperPath = "./tools/bazel" - rcFileName = ".bazeliskrc" maxDirLength = 255 ) @@ -50,6 +51,29 @@ var ( // Bazel with. type ArgsFunc func(resolvedBazelVersion string) []string +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) @@ -58,9 +82,16 @@ func RunBazelisk(args []string, repos *Repositories) (int, error) { // RunBazeliskWithArgsFunc runs the main Bazelisk logic for the given ArgsFunc and Bazel // repositories. func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error) { - httputil.UserAgent = getUserAgent() - bazeliskHome := GetEnvOrConfig("BAZELISK_HOME") + 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) { + httputil.UserAgent = getUserAgent(config) + + bazeliskHome := config.Get("BAZELISK_HOME") if len(bazeliskHome) == 0 { userCacheDir, err := os.UserCacheDir() if err != nil { @@ -75,7 +106,7 @@ func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error return -1, fmt.Errorf("could not create directory %s: %v", bazeliskHome, err) } - bazelVersionString, err := getBazelVersion() + bazelVersionString, err := getBazelVersion(config) if err != nil { return -1, fmt.Errorf("could not get Bazel version: %v", err) } @@ -92,7 +123,7 @@ func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error // 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) + bazelPath, err = downloadBazel(bazelVersionString, bazeliskHome, repos, config) if err != nil { return -1, fmt.Errorf("could not download Bazel: %v", err) } @@ -109,7 +140,7 @@ func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error // --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) + cmd := makeBazelCmd(bazelPath, args, nil, config) for _, val := range cmd.Env { fmt.Println(val) } @@ -122,12 +153,12 @@ func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error if err != nil { return -1, err } - newFlags, err := getIncompatibleFlags(bazelPath, cmd) + 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) + 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. @@ -141,7 +172,7 @@ func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error value := args[0][len("--bisect="):] commits := strings.Split(value, "..") if len(commits) == 2 { - bisect(commits[0], commits[1], args[1:], bazeliskHome, repos) + bisect(commits[0], commits[1], args[1:], bazeliskHome, repos, config) } else { return -1, fmt.Errorf("Error: Invalid format for --bisect. Expected format: '--bisect=..'") } @@ -167,7 +198,7 @@ func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error } } - exitCode, err := runBazel(bazelPath, args, nil) + exitCode, err := runBazel(bazelPath, args, nil, config) if err != nil { return -1, fmt.Errorf("could not run Bazel: %v", err) } @@ -183,130 +214,14 @@ func getBazelCommand(args []string) (string, error) { 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, " ")) } -func getUserAgent() string { - agent := GetEnvOrConfig("BAZELISK_USER_AGENT") +func getUserAgent(config config.Config) string { + agent := config.Get("BAZELISK_USER_AGENT") if len(agent) > 0 { return agent } return fmt.Sprintf("Bazelisk/%s", BazeliskVersion) } -// GetEnvOrConfig reads a configuration value from the environment, but fall back to reading it from .bazeliskrc in the workspace root. -func GetEnvOrConfig(name string) string { - if val := os.Getenv(name); val != "" { - return val - } - - fileConfigOnce.Do(loadFileConfig) - - return fileConfig[name] -} - -// loadFileConfig locates available .bazeliskrc configuration files, parses them with a precedence order preference, -// and updates a global configuration map with their contents. This routine should be executed exactly once. -func loadFileConfig() { - var rcFilePaths []string - - if userRC, err := locateUserConfigFile(); err == nil { - rcFilePaths = append(rcFilePaths, userRC) - } - if workspaceRC, err := locateWorkspaceConfigFile(); err == nil { - rcFilePaths = append(rcFilePaths, workspaceRC) - } - - fileConfig = make(map[string]string) - for _, rcPath := range rcFilePaths { - config, err := parseFileConfig(rcPath) - if err != nil { - log.Fatal(err) - } - - for key, value := range config { - fileConfig[key] = value - } - } -} - -// locateWorkspaceConfigFile locates a .bazeliskrc file in the current workspace root. -func locateWorkspaceConfigFile() (string, error) { - workingDirectory, err := os.Getwd() - if err != nil { - return "", err - } - workspaceRoot := findWorkspaceRoot(workingDirectory) - if workspaceRoot == "" { - return "", err - } - return filepath.Join(workspaceRoot, rcFileName), nil -} - -// locateUserConfigFile locates a .bazeliskrc file in the user's home directory. -func locateUserConfigFile() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, rcFileName), nil -} - -// parseFileConfig parses a .bazeliskrc file as a map of key-value configuration values. -func parseFileConfig(rcFilePath string) (map[string]string, error) { - config := make(map[string]string) - - contents, err := ioutil.ReadFile(rcFilePath) - if err != nil { - if os.IsNotExist(err) { - // Non-critical error. - return config, nil - } - return nil, err - } - - for _, line := range strings.Split(string(contents), "\n") { - if strings.HasPrefix(line, "#") { - // comments - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) < 2 { - continue - } - key := strings.TrimSpace(parts[0]) - config[key] = strings.TrimSpace(parts[1]) - } - - return config, nil -} - -// isValidWorkspace returns true iff the supplied path is the workspace root, defined by the presence of -// a file named WORKSPACE or WORKSPACE.bazel -// see https://github.com/bazelbuild/bazel/blob/8346ea4cfdd9fbd170d51a528fee26f912dad2d5/src/main/cpp/workspace_layout.cc#L37 -func isValidWorkspace(path string) bool { - info, err := os.Stat(path) - if err != nil { - return false - } - - return !info.IsDir() -} - -func findWorkspaceRoot(root string) string { - if isValidWorkspace(filepath.Join(root, "WORKSPACE")) { - return root - } - - if isValidWorkspace(filepath.Join(root, "WORKSPACE.bazel")) { - return root - } - - parentDirectory := filepath.Dir(root) - if parentDirectory == root { - return "" - } - - return findWorkspaceRoot(parentDirectory) -} - // TODO(go 1.18): remove backport of strings.Cut func cutString(s, sep string) (before, after string, found bool) { if i := strings.Index(s, sep); i >= 0 { @@ -315,7 +230,7 @@ func cutString(s, sep string) (before, after string, found bool) { return s, "", false } -func getBazelVersion() (string, error) { +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: @@ -331,7 +246,7 @@ func getBazelVersion() (string, error) { // - 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 := GetEnvOrConfig("USE_BAZEL_VERSION") + bazelVersion := config.Get("USE_BAZEL_VERSION") if len(bazelVersion) != 0 { return bazelVersion, nil } @@ -341,7 +256,7 @@ func getBazelVersion() (string, error) { return "", fmt.Errorf("could not get working directory: %v", err) } - workspaceRoot := findWorkspaceRoot(workingDirectory) + workspaceRoot := ws.FindWorkspaceRoot(workingDirectory) if len(workspaceRoot) != 0 { bazelVersionPath := filepath.Join(workspaceRoot, ".bazelversion") if _, err := os.Stat(bazelVersionPath); err == nil { @@ -364,7 +279,7 @@ func getBazelVersion() (string, error) { } } - fallbackVersionFormat := GetEnvOrConfig("USE_BAZEL_FALLBACK_VERSION") + fallbackVersionFormat := config.Get("USE_BAZEL_FALLBACK_VERSION") fallbackVersionMode, fallbackVersion, hasFallbackVersionMode := cutString(fallbackVersionFormat, ":") if !hasFallbackVersionMode { fallbackVersionMode, fallbackVersion, hasFallbackVersionMode = "silent", fallbackVersionMode, true @@ -401,7 +316,7 @@ func parseBazelForkAndVersion(bazelForkAndVersion string) (string, string, error return bazelFork, bazelVersion, nil } -func downloadBazel(bazelVersionString string, bazeliskHome string, repos *Repositories) (string, error) { +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) @@ -412,12 +327,12 @@ func downloadBazel(bazelVersionString string, bazeliskHome string, repos *Reposi return "", fmt.Errorf("could not resolve the version '%s' to an actual version number: %v", bazelVersion, err) } - bazelForkOrURL := dirForURL(GetEnvOrConfig(BaseURLEnv)) + bazelForkOrURL := dirForURL(config.Get(BaseURLEnv)) if len(bazelForkOrURL) == 0 { bazelForkOrURL = bazelFork } - bazelPath, err := downloadBazelIfNecessary(resolvedBazelVersion, bazeliskHome, bazelForkOrURL, repos, downloader) + bazelPath, err := downloadBazelIfNecessary(resolvedBazelVersion, bazeliskHome, bazelForkOrURL, repos, config, downloader) return bazelPath, err } @@ -430,11 +345,12 @@ func downloadBazel(bazelVersionString string, bazeliskHome string, repos *Reposi // // 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, downloader DownloadFunc) (string, error) { +func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrURLDirName string, repos *Repositories, config config.Config, downloader DownloadFunc) (string, error) { pathSegment, err := platforms.DetermineBazelFilename(version, false) 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) @@ -446,12 +362,12 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR } } - pathToBazelInCAS, downloadedDigest, err := downloadBazelToCAS(version, bazeliskHome, repos, downloader) + pathToBazelInCAS, downloadedDigest, err := downloadBazelToCAS(version, bazeliskHome, repos, config, downloader) if err != nil { return "", fmt.Errorf("failed to download bazel: %w", err) } - expectedSha256 := strings.ToLower(GetEnvOrConfig("BAZELISK_VERIFY_SHA256")) + 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) @@ -479,7 +395,7 @@ func atomicWriteFile(path string, contents []byte, perm os.FileMode) error { return nil } -func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories, downloader DownloadFunc) (string, string, error) { +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") @@ -492,12 +408,12 @@ func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories var tmpDestPath string var err error - baseURL := GetEnvOrConfig(BaseURLEnv) - formatURL := GetEnvOrConfig(FormatURLEnv) + 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(formatURL, version, temporaryDownloadDir, tmpDestFile) + tmpDestPath, err = repos.DownloadFromFormatURL(config, formatURL, version, temporaryDownloadDir, tmpDestFile) } else if baseURL != "" { tmpDestPath, err = repos.DownloadFromBaseURL(baseURL, version, temporaryDownloadDir, tmpDestFile) } else { @@ -575,24 +491,24 @@ func linkLocalBazel(baseDirectory string, bazelPath string) (string, error) { return destinationPath, nil } -func maybeDelegateToWrapperFromDir(bazel string, wd string, ignoreEnv bool) string { - if !ignoreEnv && GetEnvOrConfig(skipWrapperEnv) != "" { +func maybeDelegateToWrapperFromDir(bazel string, wd string, config config.Config) string { + if config.Get(skipWrapperEnv) != "" { return bazel } - root := findWorkspaceRoot(wd) + 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") + 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") + batchWrapper := filepath.Join(root, wrapperPath+".bat") if stat, err := os.Stat(batchWrapper); err == nil && !stat.Mode().IsDir() { return batchWrapper } @@ -601,13 +517,13 @@ func maybeDelegateToWrapperFromDir(bazel string, wd string, ignoreEnv bool) stri return bazel } -func maybeDelegateToWrapper(bazel string) string { +func maybeDelegateToWrapper(bazel string, config config.Config) string { wd, err := os.Getwd() if err != nil { return bazel } - return maybeDelegateToWrapperFromDir(bazel, wd, false) + return maybeDelegateToWrapperFromDir(bazel, wd, config) } func prependDirToPathList(cmd *exec.Cmd, dir string) { @@ -629,8 +545,8 @@ func prependDirToPathList(cmd *exec.Cmd, dir string) { } } -func makeBazelCmd(bazel string, args []string, out io.Writer) *exec.Cmd { - execPath := maybeDelegateToWrapper(bazel) +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") @@ -648,8 +564,8 @@ func makeBazelCmd(bazel string, args []string, out io.Writer) *exec.Cmd { return cmd } -func runBazel(bazel string, args []string, out io.Writer) (int, error) { - cmd := makeBazelCmd(bazel, args, out) +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) @@ -678,14 +594,14 @@ func runBazel(bazel string, args []string, out io.Writer) (int, error) { } // getIncompatibleFlags returns all incompatible flags for the current Bazel command in alphabetical order. -func getIncompatibleFlags(bazelPath, cmd string) ([]string, error) { - var incompatibleFlagsStr = GetEnvOrConfig("BAZELISK_INCOMPATIBLE_FLAGS") +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); err != nil { + 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) } @@ -721,27 +637,27 @@ func insertArgs(baseArgs []string, newArgs []string) []string { func parseStartupOptions(baseArgs []string) []string { var result []string var bazelCommands = map[string]bool{ - "analyze-profile": true, - "aquery": true, - "build": true, + "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, + "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 { @@ -753,15 +669,15 @@ func parseStartupOptions(baseArgs []string) []string { return result } -func shutdownIfNeeded(bazelPath string, startupOptions []string) { - bazeliskClean := GetEnvOrConfig("BAZELISK_SHUTDOWN") +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) + exitCode, err := runBazel(bazelPath, args, nil, config) fmt.Printf("\n") if err != nil { log.Fatalf("failed to run bazel shutdown: %v", err) @@ -772,15 +688,15 @@ func shutdownIfNeeded(bazelPath string, startupOptions []string) { } } -func cleanIfNeeded(bazelPath string, startupOptions []string) { - bazeliskClean := GetEnvOrConfig("BAZELISK_CLEAN") +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) + exitCode, err := runBazel(bazelPath, args, nil, config) fmt.Printf("\n") if err != nil { log.Fatalf("failed to run clean: %v", err) @@ -796,17 +712,17 @@ type ParentCommit struct { } type Commit struct { - SHA string `json:"sha"` + 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"` + Commits []Commit `json:"commits"` + BaseCommit Commit `json:"base_commit"` + MergeBaseCommit Commit `json:"merge_base_commit"` } -func sendRequest(url string) (*http.Response, error) { +func sendRequest(url string, config config.Config) (*http.Response, error) { client := &http.Client{} req, err := http.NewRequest("GET", url, nil) @@ -814,7 +730,7 @@ func sendRequest(url string) (*http.Response, error) { return nil, err } - githubToken := GetEnvOrConfig("BAZELISK_GITHUB_TOKEN") + githubToken := config.Get("BAZELISK_GITHUB_TOKEN") if len(githubToken) != 0 { req.Header.Set("Authorization", fmt.Sprintf("token %s", githubToken)) } @@ -822,7 +738,7 @@ func sendRequest(url string) (*http.Response, error) { return client.Do(req) } -func getBazelCommitsBetween(goodCommit string, badCommit string) (string, []string, error) { +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 @@ -830,7 +746,7 @@ func getBazelCommitsBetween(goodCommit string, badCommit string) (string, []stri 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) + response, err := sendRequest(url, config) if err != nil { return goodCommit, nil, fmt.Errorf("Error fetching commit data: %v", err) } @@ -887,11 +803,11 @@ func getBazelCommitsBetween(goodCommit string, badCommit string) (string, []stri return goodCommit, commitList, nil } -func bisect(goodCommit string, badCommit string, args []string, bazeliskHome string, repos *Repositories) { +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) + goodCommit, commitList, err := getBazelCommitsBetween(goodCommit, badCommit, config) if err != nil { log.Fatalf("Failed to get commits: %v", err) os.Exit(1) @@ -899,7 +815,7 @@ func bisect(goodCommit string, badCommit string, args []string, bazeliskHome str // 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) + bazelExitCode, err := testWithBazelAtCommit(goodCommit, args, bazeliskHome, repos, config) if err != nil { log.Fatalf("could not run Bazel: %v", err) os.Exit(1) @@ -916,8 +832,8 @@ func bisect(goodCommit string, badCommit string, args []string, bazeliskHome str 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) + 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) @@ -943,16 +859,16 @@ func bisect(goodCommit string, badCommit string, args []string, bazeliskHome str os.Exit(0) } -func testWithBazelAtCommit(bazelCommit string, args []string, bazeliskHome string, repos *Repositories) (int, error) { - bazelPath, err := downloadBazel(bazelCommit, bazeliskHome, repos) +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) - cleanIfNeeded(bazelPath, startupOptions) + shutdownIfNeeded(bazelPath, startupOptions, config) + cleanIfNeeded(bazelPath, startupOptions, config) fmt.Printf("bazel %s\n", strings.Join(args, " ")) - bazelExitCode, err := runBazel(bazelPath, args, nil) + bazelExitCode, err := runBazel(bazelPath, args, nil, config) if err != nil { return -1, fmt.Errorf("could not run Bazel: %v", err) } @@ -960,16 +876,16 @@ func testWithBazelAtCommit(bazelCommit string, args []string, bazeliskHome strin } // migrate will run Bazel with each flag separately and report which ones are failing. -func migrate(bazelPath string, baseArgs []string, flags []string) { +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) - cleanIfNeeded(bazelPath, startupOptions) + shutdownIfNeeded(bazelPath, startupOptions, config) + cleanIfNeeded(bazelPath, startupOptions, config) fmt.Printf("bazel %s\n", strings.Join(args, " ")) - exitCode, err := runBazel(bazelPath, args, nil) + exitCode, err := runBazel(bazelPath, args, nil, config) if err != nil { log.Fatalf("could not run Bazel: %v", err) } @@ -981,10 +897,10 @@ func migrate(bazelPath string, baseArgs []string, flags []string) { // 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) - cleanIfNeeded(bazelPath, startupOptions) + shutdownIfNeeded(bazelPath, startupOptions, config) + cleanIfNeeded(bazelPath, startupOptions, config) fmt.Printf("bazel %s\n", strings.Join(args, " ")) - exitCode, err = runBazel(bazelPath, args, nil) + exitCode, err = runBazel(bazelPath, args, nil, config) if err != nil { log.Fatalf("could not run Bazel: %v", err) } @@ -999,10 +915,10 @@ func migrate(bazelPath string, baseArgs []string, flags []string) { for _, arg := range flags { args = insertArgs(baseArgs, []string{arg}) fmt.Printf("\n\n--- Running Bazel with %s\n\n", arg) - shutdownIfNeeded(bazelPath, startupOptions) - cleanIfNeeded(bazelPath, startupOptions) + shutdownIfNeeded(bazelPath, startupOptions, config) + cleanIfNeeded(bazelPath, startupOptions, config) fmt.Printf("bazel %s\n", strings.Join(args, " ")) - exitCode, err = runBazel(bazelPath, args, nil) + exitCode, err = runBazel(bazelPath, args, nil, config) if err != nil { log.Fatalf("could not run Bazel: %v", err) } diff --git a/core/core_test.go b/core/core_test.go index 9f1dea88..75424f28 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -7,6 +7,8 @@ import ( "path/filepath" "runtime" "testing" + + "github.com/bazelbuild/bazelisk/config" ) func TestMaybeDelegateToNoWrapper(t *testing.T) { @@ -16,11 +18,11 @@ func TestMaybeDelegateToNoWrapper(t *testing.T) { } defer os.RemoveAll(tmpDir) - os.MkdirAll(tmpDir, os.ModeDir | 0700) + os.MkdirAll(tmpDir, os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600) ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600) - entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true) + entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, config.Null()) expected := "bazel_real" if entrypoint != expected { @@ -35,21 +37,21 @@ func TestMaybeDelegateToNoNonExecutableWrapper(t *testing.T) { if runtime.GOOS == "windows" { return } - + tmpDir, err := ioutil.TempDir("", "TestMaybeDelegateToNoNonExecutableWrapper") if err != nil { log.Fatal(err) } defer os.RemoveAll(tmpDir) - os.MkdirAll(tmpDir, os.ModeDir | 0700) + os.MkdirAll(tmpDir, os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600) ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600) - os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700) + os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel"), []byte(""), 0600) - entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true) + entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, config.Null()) expected := "bazel_real" if entrypoint != expected { @@ -71,14 +73,14 @@ func TestMaybeDelegateToStandardWrapper(t *testing.T) { } defer os.RemoveAll(tmpDir) - os.MkdirAll(tmpDir, os.ModeDir | 0700) + os.MkdirAll(tmpDir, os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600) ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600) - os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700) + os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel"), []byte(""), 0700) - entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true) + entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, config.Null()) expected := filepath.Join(tmpDir, "tools", "bazel") if entrypoint != expected { @@ -93,14 +95,14 @@ func TestMaybeDelegateToPowershellWrapper(t *testing.T) { } defer os.RemoveAll(tmpDir) - os.MkdirAll(tmpDir, os.ModeDir | 0700) + os.MkdirAll(tmpDir, os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600) ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600) - os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700) + os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel.ps1"), []byte(""), 0700) - entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true) + entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, config.Null()) expected := filepath.Join(tmpDir, "tools", "bazel.ps1") // Only windows platforms use powershell wrappers @@ -120,14 +122,14 @@ func TestMaybeDelegateToBatchWrapper(t *testing.T) { } defer os.RemoveAll(tmpDir) - os.MkdirAll(tmpDir, os.ModeDir | 0700) + os.MkdirAll(tmpDir, os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600) ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600) - os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700) + os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel.bat"), []byte(""), 0700) - entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true) + entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, config.Null()) expected := filepath.Join(tmpDir, "tools", "bazel.bat") // Only windows platforms use batch wrappers @@ -147,15 +149,15 @@ func TestMaybeDelegateToPowershellOverBatchWrapper(t *testing.T) { } defer os.RemoveAll(tmpDir) - os.MkdirAll(tmpDir, os.ModeDir | 0700) + os.MkdirAll(tmpDir, os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "WORKSPACE"), []byte(""), 0600) ioutil.WriteFile(filepath.Join(tmpDir, "BUILD"), []byte(""), 0600) - os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir | 0700) + os.MkdirAll(filepath.Join(tmpDir, "tools"), os.ModeDir|0700) ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel.ps1"), []byte(""), 0700) ioutil.WriteFile(filepath.Join(tmpDir, "tools", "bazel.bat"), []byte(""), 0700) - entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, true) + entrypoint := maybeDelegateToWrapperFromDir("bazel_real", tmpDir, config.Null()) expected := filepath.Join(tmpDir, "tools", "bazel.ps1") // Only windows platforms use powershell or batch wrappers diff --git a/core/repositories.go b/core/repositories.go index 7b5b4be6..e11e4460 100644 --- a/core/repositories.go +++ b/core/repositories.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/bazelbuild/bazelisk/config" "github.com/bazelbuild/bazelisk/httputil" "github.com/bazelbuild/bazelisk/platforms" "github.com/bazelbuild/bazelisk/versions" @@ -235,7 +236,7 @@ func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile s return httputil.DownloadBinary(url, destDir, destFile) } -func BuildURLFromFormat(formatURL, version string) (string, error) { +func BuildURLFromFormat(config config.Config, formatURL, version string) (string, error) { osName, err := platforms.DetermineOperatingSystem() if err != nil { return "", err @@ -261,7 +262,7 @@ func BuildURLFromFormat(formatURL, version string) (string, error) { case 'e': b.WriteString(platforms.DetermineExecutableFilenameSuffix()) case 'h': - b.WriteString(GetEnvOrConfig("BAZELISK_VERIFY_SHA256")) + b.WriteString(config.Get("BAZELISK_VERIFY_SHA256")) case 'm': b.WriteString(machineName) case 'o': @@ -281,12 +282,12 @@ func BuildURLFromFormat(formatURL, version string) (string, error) { } // DownloadFromFormatURL can download Bazel binaries from a specific URL while ignoring the predefined repositories. -func (r *Repositories) DownloadFromFormatURL(formatURL, version, destDir, destFile string) (string, error) { +func (r *Repositories) DownloadFromFormatURL(config config.Config, formatURL, version, destDir, destFile string) (string, error) { if formatURL == "" { return "", fmt.Errorf("%s is not set", FormatURLEnv) } - url, err := BuildURLFromFormat(formatURL, version) + url, err := BuildURLFromFormat(config, formatURL, version) if err != nil { return "", err } diff --git a/core/repositories_test.go b/core/repositories_test.go index f6442446..27ee65bc 100644 --- a/core/repositories_test.go +++ b/core/repositories_test.go @@ -3,9 +3,9 @@ package core import ( "errors" "fmt" - "os" "testing" + "github.com/bazelbuild/bazelisk/config" "github.com/bazelbuild/bazelisk/platforms" ) @@ -24,18 +24,10 @@ func TestBuildURLFromFormat(t *testing.T) { suffix := platforms.DetermineExecutableFilenameSuffix() - previousSha256, hadSha256 := os.LookupEnv("BAZELISK_VERIFY_SHA256") sha256 := "SomeSha256ValueThatIsIrrelevant" - if err := os.Setenv("BAZELISK_VERIFY_SHA256", sha256); err != nil { - t.Fatalf("Failed to set BAZELISK_VERIFY_SHA256") - } - defer func() { - if hadSha256 { - os.Setenv("BAZELISK_VERIFY_SHA256", previousSha256) - } else { - os.Unsetenv("BAZELISK_VERIFY_SHA256") - } - }() + config := config.Static(map[string]string{ + "BAZELISK_VERIFY_SHA256": sha256, + }) type test struct { format string @@ -65,7 +57,7 @@ func TestBuildURLFromFormat(t *testing.T) { } for _, tc := range tests { - got, err := BuildURLFromFormat(tc.format, version) + got, err := BuildURLFromFormat(config, tc.format, version) if fmt.Sprintf("%v", err) != fmt.Sprintf("%v", tc.wantErr) { if got != "" { t.Errorf("format '%s': got non-empty '%s' on error", tc.format, got) diff --git a/ws/BUILD b/ws/BUILD new file mode 100644 index 00000000..8b3ebbf7 --- /dev/null +++ b/ws/BUILD @@ -0,0 +1,11 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["ws.go"], + importpath = "github.com/bazelbuild/bazelisk/ws", + visibility = ["//visibility:public"], + deps = [ + "@com_github_mitchellh_go_homedir//:go_default_library", + ], +) diff --git a/ws/ws.go b/ws/ws.go new file mode 100644 index 00000000..653090de --- /dev/null +++ b/ws/ws.go @@ -0,0 +1,36 @@ +package ws + +import ( + "os" + "path/filepath" +) + +// FindWorkspaceRoot returns the root directory of the Bazel workspace in which the passed root exists, if any. +func FindWorkspaceRoot(root string) string { + if isValidWorkspace(filepath.Join(root, "WORKSPACE")) { + return root + } + + if isValidWorkspace(filepath.Join(root, "WORKSPACE.bazel")) { + return root + } + + parentDirectory := filepath.Dir(root) + if parentDirectory == root { + return "" + } + + return FindWorkspaceRoot(parentDirectory) +} + +// isValidWorkspace returns true iff the supplied path is the workspace root, defined by the presence of +// a file named WORKSPACE or WORKSPACE.bazel +// see https://github.com/bazelbuild/bazel/blob/8346ea4cfdd9fbd170d51a528fee26f912dad2d5/src/main/cpp/workspace_layout.cc#L37 +func isValidWorkspace(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + + return !info.IsDir() +}