feat: add fetching flags from bazel (#18)
diff --git a/WORKSPACE b/WORKSPACE index 1e8310f..6156665 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -43,6 +43,19 @@ ], ) +http_archive( + name = "rules_proto", + sha256 = "9fc210a34f0f9e7cc31598d109b5d069ef44911a82f507d5a88716db171615a8", + strip_prefix = "rules_proto-f7a30f6f80006b591fa7c437fe5a951eb10bcbcf", + urls = ["https://github.com/bazelbuild/rules_proto/archive/f7a30f6f80006b591fa7c437fe5a951eb10bcbcf.tar.gz"], +) + +load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains") + +rules_proto_dependencies() + +rules_proto_toolchains() + load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") load("//:go.bzl", _go_repositories = "deps")
diff --git a/go.mod b/go.mod index 2805070..95b6572 100644 --- a/go.mod +++ b/go.mod
@@ -25,5 +25,6 @@ golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 google.golang.org/grpc v1.27.0 + google.golang.org/protobuf v1.26.0 gopkg.in/ini.v1 v1.62.0 // indirect )
diff --git a/pkg/bazel/BUILD.bazel b/pkg/bazel/BUILD.bazel index f1ad4b9..21a9a6f 100644 --- a/pkg/bazel/BUILD.bazel +++ b/pkg/bazel/BUILD.bazel
@@ -1,16 +1,25 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto//proto:defs.bzl", "proto_library") go_library( name = "bazel", srcs = [ "bazel.go", + "bazelisk.go", "flags.go", ], + embed = [":bazel_go_proto"], importpath = "aspect.build/cli/pkg/bazel", visibility = ["//:__subpackages__"], deps = [ "@com_github_bazelbuild_bazelisk//core:go_default_library", + "@com_github_bazelbuild_bazelisk//httputil:go_default_library", + "@com_github_bazelbuild_bazelisk//platforms:go_default_library", "@com_github_bazelbuild_bazelisk//repositories:go_default_library", + "@com_github_bazelbuild_bazelisk//versions:go_default_library", + "@com_github_mitchellh_go_homedir//:go-homedir", + "@org_golang_google_protobuf//proto", ], ) @@ -22,3 +31,16 @@ "@com_github_onsi_gomega//:gomega", ], ) + +proto_library( + name = "bazel_proto", + srcs = ["flags.proto"], + visibility = ["//visibility:public"], +) + +go_proto_library( + name = "bazel_go_proto", + importpath = "aspect.build/cli/pkg/bazel", + proto = ":bazel_proto", + visibility = ["//visibility:public"], +)
diff --git a/pkg/bazel/bazel.go b/pkg/bazel/bazel.go index b45948c..894cbd6 100644 --- a/pkg/bazel/bazel.go +++ b/pkg/bazel/bazel.go
@@ -9,6 +9,7 @@ import ( "github.com/bazelbuild/bazelisk/core" "github.com/bazelbuild/bazelisk/repositories" + "io" ) type Spawner interface { @@ -22,15 +23,22 @@ return &Bazel{} } -// Spawn is similar to the main() function of bazelisk -// see https://github.com/bazelbuild/bazelisk/blob/7c3d9d5/bazelisk.go -func (*Bazel) Spawn(command []string) (int, error) { +func (*Bazel) createRepositories() *core.Repositories { gcs := &repositories.GCSRepo{} gitHub := repositories.CreateGitHubRepo(core.GetEnvOrConfig("BAZELISK_GITHUB_TOKEN")) // Fetch LTS releases, release candidates and Bazel-at-commits from GCS, forks and rolling releases from GitHub. // TODO(https://github.com/bazelbuild/bazelisk/issues/228): get rolling releases from GCS, too. - repos := core.CreateRepositories(gcs, gcs, gitHub, gcs, gitHub, true) + return core.CreateRepositories(gcs, gcs, gitHub, gcs, gitHub, true) +} - exitCode, err := core.RunBazelisk(command, repos) +// Spawn is similar to the main() function of bazelisk +// see https://github.com/bazelbuild/bazelisk/blob/7c3d9d5/bazelisk.go +func (b *Bazel) Spawn(command []string) (int, error) { + return b.RunCommand(command, nil) +} + +func (b *Bazel) RunCommand(command []string, out io.Writer) (int, error) { + repos := b.createRepositories() + exitCode, err := RunBazelisk(command, repos, out) return exitCode, err }
diff --git a/pkg/bazel/bazelisk.go b/pkg/bazel/bazelisk.go new file mode 100644 index 0000000..ced2bb9 --- /dev/null +++ b/pkg/bazel/bazelisk.go
@@ -0,0 +1,600 @@ +package bazel + +import ( + "bufio" + "fmt" + "github.com/bazelbuild/bazelisk/core" + "github.com/bazelbuild/bazelisk/httputil" + "github.com/bazelbuild/bazelisk/platforms" + "github.com/bazelbuild/bazelisk/versions" + "github.com/mitchellh/go-homedir" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "os/signal" + "path/filepath" + "regexp" + "runtime" + "sort" + "strings" + "sync" + "syscall" +) + +const ( + bazelReal = "BAZEL_REAL" + skipWrapperEnv = "BAZELISK_SKIP_WRAPPER" + wrapperPath = "./tools/bazel" +) + +var ( + // BazeliskVersion is filled in via x_defs when building a release. + BazeliskVersion = "development" + + fileConfig map[string]string + fileConfigOnce sync.Once +) + +// RunBazelisk runs the main Bazelisk logic for the given arguments and Bazel repositories. +func RunBazelisk(args []string, repos *core.Repositories, out io.Writer) (int, error) { + httputil.UserAgent = getUserAgent() + + bazeliskHome := GetEnvOrConfig("BAZELISK_HOME") + if len(bazeliskHome) == 0 { + userCacheDir, err := os.UserCacheDir() + if err != nil { + return -1, fmt.Errorf("could not get the user's cache directory: %v", err) + } + + bazeliskHome = filepath.Join(userCacheDir, "bazelisk") + } + + err := os.MkdirAll(bazeliskHome, 0755) + if err != nil { + return -1, fmt.Errorf("could not create directory %s: %v", bazeliskHome, err) + } + + bazelVersionString, err := getBazelVersion() + 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) { + bazelFork, bazelVersion, err := parseBazelForkAndVersion(bazelVersionString) + if err != nil { + return -1, fmt.Errorf("could not parse Bazel fork and version: %v", err) + } + + var downloader core.DownloadFunc + resolvedBazelVersion, downloader, err = repos.ResolveVersion(bazeliskHome, bazelFork, bazelVersion) + if err != nil { + return -1, fmt.Errorf("could not resolve the version '%s' to an actual version number: %v", bazelVersion, err) + } + + bazelForkOrURL := dirForURL(GetEnvOrConfig(core.BaseURLEnv)) + if len(bazelForkOrURL) == 0 { + bazelForkOrURL = bazelFork + } + + baseDirectory := filepath.Join(bazeliskHome, "downloads", bazelForkOrURL) + bazelPath, err = downloadBazel(bazelFork, resolvedBazelVersion, baseDirectory, repos, downloader) + 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("cound not link local Bazel: %v", err) + } + } + + // --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) + for _, val := range cmd.Env { + fmt.Println(val) + } + return 0, nil + } + + // --strict and --migrate 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) + 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) + } 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) + } + } + + // 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) + 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, " ")) +} + +func getUserAgent() string { + agent := GetEnvOrConfig("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 + } + + // Parse .bazeliskrc in the workspace root, once, if it can be found. + fileConfigOnce.Do(func() { + workingDirectory, err := os.Getwd() + if err != nil { + return + } + workspaceRoot := findWorkspaceRoot(workingDirectory) + if workspaceRoot == "" { + return + } + rcFilePath := filepath.Join(workspaceRoot, ".bazeliskrc") + contents, err := ioutil.ReadFile(rcFilePath) + if err != nil { + if os.IsNotExist(err) { + return + } + log.Fatal(err) + } + fileConfig = make(map[string]string) + 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]) + fileConfig[key] = strings.TrimSpace(parts[1]) + } + }) + + return fileConfig[name] +} + +// 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) +} + +func getBazelVersion() (string, error) { + // Check in this order: + // - env 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/.bazeliskrc exists and contains a 'USE_BAZEL_VERSION' + // variable -> read contents, that version. + // - workspace_root/.bazelversion exists -> read contents, that version. + // - workspace_root/WORKSPACE contains a version -> that version. (TODO) + // - fallback: latest release + bazelVersion := GetEnvOrConfig("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 := 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 + } + } + } + + return "latest", nil +} + +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(fork string, version string, baseDirectory string, repos *core.Repositories, downloader core.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() + destinationDir := filepath.Join(baseDirectory, pathSegment, "bin") + + if url := GetEnvOrConfig(core.BaseURLEnv); url != "" { + return repos.DownloadFromBaseURL(url, version, destinationDir, destFile) + } + + return downloader(destinationDir, destFile) +} + +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("cound not copy file from %s to %s: %v", bazelPath, destinationPath, err) + } + } + } + return destinationPath, nil +} + +func maybeDelegateToWrapper(bazel string) string { + if GetEnvOrConfig(skipWrapperEnv) != "" { + return bazel + } + + wd, err := os.Getwd() + if err != nil { + return bazel + } + + root := findWorkspaceRoot(wd) + wrapper := filepath.Join(root, wrapperPath) + if stat, err := os.Stat(wrapper); err != nil || stat.IsDir() || stat.Mode().Perm()&0001 == 0 { + return bazel + } + + return wrapper +} + +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) *exec.Cmd { + execPath := maybeDelegateToWrapper(bazel) + + 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) (int, error) { + cmd := makeBazelCmd(bazel, args, out) + 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 + if runtime.GOOS != "windows" { + cmd.Process.Signal(s) + } else { + 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) ([]string, error) { + out := strings.Builder{} + if _, err := runBazel(bazelPath, []string{"help", cmd, "--short"}, &out); 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 shutdownIfNeeded(bazelPath string) { + bazeliskClean := GetEnvOrConfig("BAZELISK_SHUTDOWN") + if len(bazeliskClean) == 0 { + return + } + + fmt.Printf("bazel shutdown\n") + exitCode, err := runBazel(bazelPath, []string{"shutdown"}, nil) + 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) { + bazeliskClean := GetEnvOrConfig("BAZELISK_CLEAN") + if len(bazeliskClean) == 0 { + return + } + + fmt.Printf("bazel clean --expunge\n") + exitCode, err := runBazel(bazelPath, []string{"clean", "--expunge"}, nil) + 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) + } +} + +// migrate will run Bazel with each flag separately and report which ones are failing. +func migrate(bazelPath string, baseArgs []string, flags []string) { + // 1. Try with all the flags. + args := insertArgs(baseArgs, flags) + fmt.Printf("\n\n--- Running Bazel with all incompatible flags\n\n") + shutdownIfNeeded(bazelPath) + cleanIfNeeded(bazelPath) + fmt.Printf("bazel %s\n", strings.Join(args, " ")) + exitCode, err := runBazel(bazelPath, args, nil) + 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) + cleanIfNeeded(bazelPath) + fmt.Printf("bazel %s\n", strings.Join(args, " ")) + exitCode, err = runBazel(bazelPath, args, nil) + 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) + cleanIfNeeded(bazelPath) + fmt.Printf("bazel %s\n", strings.Join(args, " ")) + exitCode, err = runBazel(bazelPath, args, nil) + 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 "-". + return regexp.MustCompile("[[:^alnum:]]").ReplaceAllString(url, "-") +}
diff --git a/pkg/bazel/flags.go b/pkg/bazel/flags.go index 4ff1cfc..9051e67 100644 --- a/pkg/bazel/flags.go +++ b/pkg/bazel/flags.go
@@ -5,3 +5,45 @@ */ package bazel + +import ( + "encoding/base64" + "fmt" + "io" + "io/ioutil" + + "google.golang.org/protobuf/proto" +) + +func (b *Bazel) Flags() (map[string]*FlagInfo, error) { + r, w := io.Pipe() + decoder := base64.NewDecoder(base64.StdEncoding, r) + bazelErrs := make(chan error, 1) + defer close(bazelErrs) + go func() { + defer w.Close() + _, err := b.RunCommand([]string{"help", "flags-as-proto"}, w) + bazelErrs <- err + }() + + helpProtoBytes, err := ioutil.ReadAll(decoder) + if err != nil { + return nil, fmt.Errorf("failed to get Bazel flags: %w", err) + } + + if err := <-bazelErrs; err != nil { + return nil, fmt.Errorf("failed to get Bazel flags: %w", err) + } + + flagCollection := &FlagCollection{} + if err := proto.Unmarshal(helpProtoBytes, flagCollection); err != nil { + return nil, fmt.Errorf("failed to get Bazel flags: %w", err) + } + + flags := make(map[string]*FlagInfo) + for i := range flagCollection.FlagInfos { + flags[*flagCollection.FlagInfos[i].Name] = flagCollection.FlagInfos[i] + } + + return flags, nil +}
diff --git a/pkg/bazel/flags.pb.go b/pkg/bazel/flags.pb.go new file mode 100755 index 0000000..576e280 --- /dev/null +++ b/pkg/bazel/flags.pb.go
@@ -0,0 +1,269 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.13.0 +// source: pkg/bazel/flags.proto + +package bazel + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type FlagInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name *string `protobuf:"bytes,1,req,name=name" json:"name,omitempty"` + HasNegativeFlag *bool `protobuf:"varint,2,opt,name=has_negative_flag,json=hasNegativeFlag,def=0" json:"has_negative_flag,omitempty"` + Documentation *string `protobuf:"bytes,3,opt,name=documentation" json:"documentation,omitempty"` + Commands []string `protobuf:"bytes,4,rep,name=commands" json:"commands,omitempty"` + Abbreviation *string `protobuf:"bytes,5,opt,name=abbreviation" json:"abbreviation,omitempty"` + AllowsMultiple *bool `protobuf:"varint,6,opt,name=allows_multiple,json=allowsMultiple,def=0" json:"allows_multiple,omitempty"` +} + +// Default values for FlagInfo fields. +const ( + Default_FlagInfo_HasNegativeFlag = bool(false) + Default_FlagInfo_AllowsMultiple = bool(false) +) + +func (x *FlagInfo) Reset() { + *x = FlagInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_bazel_flags_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FlagInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlagInfo) ProtoMessage() {} + +func (x *FlagInfo) ProtoReflect() protoreflect.Message { + mi := &file_pkg_bazel_flags_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FlagInfo.ProtoReflect.Descriptor instead. +func (*FlagInfo) Descriptor() ([]byte, []int) { + return file_pkg_bazel_flags_proto_rawDescGZIP(), []int{0} +} + +func (x *FlagInfo) GetName() string { + if x != nil && x.Name != nil { + return *x.Name + } + return "" +} + +func (x *FlagInfo) GetHasNegativeFlag() bool { + if x != nil && x.HasNegativeFlag != nil { + return *x.HasNegativeFlag + } + return Default_FlagInfo_HasNegativeFlag +} + +func (x *FlagInfo) GetDocumentation() string { + if x != nil && x.Documentation != nil { + return *x.Documentation + } + return "" +} + +func (x *FlagInfo) GetCommands() []string { + if x != nil { + return x.Commands + } + return nil +} + +func (x *FlagInfo) GetAbbreviation() string { + if x != nil && x.Abbreviation != nil { + return *x.Abbreviation + } + return "" +} + +func (x *FlagInfo) GetAllowsMultiple() bool { + if x != nil && x.AllowsMultiple != nil { + return *x.AllowsMultiple + } + return Default_FlagInfo_AllowsMultiple +} + +type FlagCollection struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FlagInfos []*FlagInfo `protobuf:"bytes,1,rep,name=flag_infos,json=flagInfos" json:"flag_infos,omitempty"` +} + +func (x *FlagCollection) Reset() { + *x = FlagCollection{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_bazel_flags_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FlagCollection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlagCollection) ProtoMessage() {} + +func (x *FlagCollection) ProtoReflect() protoreflect.Message { + mi := &file_pkg_bazel_flags_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FlagCollection.ProtoReflect.Descriptor instead. +func (*FlagCollection) Descriptor() ([]byte, []int) { + return file_pkg_bazel_flags_proto_rawDescGZIP(), []int{1} +} + +func (x *FlagCollection) GetFlagInfos() []*FlagInfo { + if x != nil { + return x.FlagInfos + } + return nil +} + +var File_pkg_bazel_flags_proto protoreflect.FileDescriptor + +var file_pkg_bazel_flags_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x70, 0x6b, 0x67, 0x2f, 0x62, 0x61, 0x7a, 0x65, 0x6c, 0x2f, 0x66, 0x6c, 0x61, 0x67, + 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x62, 0x61, 0x7a, 0x65, 0x6c, 0x22, 0xe7, + 0x01, 0x0a, 0x08, 0x46, 0x6c, 0x61, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x02, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x31, 0x0a, 0x11, 0x68, 0x61, 0x73, 0x5f, 0x6e, 0x65, 0x67, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, + 0x66, 0x6c, 0x61, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x3a, 0x05, 0x66, 0x61, 0x6c, 0x73, + 0x65, 0x52, 0x0f, 0x68, 0x61, 0x73, 0x4e, 0x65, 0x67, 0x61, 0x74, 0x69, 0x76, 0x65, 0x46, 0x6c, + 0x61, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x6f, 0x63, 0x75, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x62, 0x62, 0x72, 0x65, 0x76, 0x69, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x62, 0x62, 0x72, + 0x65, 0x76, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x73, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x08, 0x3a, 0x05, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x52, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x73, + 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x22, 0x40, 0x0a, 0x0e, 0x46, 0x6c, 0x61, 0x67, + 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x0a, 0x66, 0x6c, + 0x61, 0x67, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, + 0x2e, 0x62, 0x61, 0x7a, 0x65, 0x6c, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x09, 0x66, 0x6c, 0x61, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x42, 0x47, 0x0a, 0x34, 0x63, 0x6f, + 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x64, 0x65, 0x76, 0x74, 0x6f, 0x6f, 0x6c, + 0x73, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x72, 0x75, 0x6e, 0x74, + 0x69, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x42, 0x0f, 0x42, 0x61, 0x7a, 0x65, 0x6c, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x50, 0x72, + 0x6f, 0x74, 0x6f, +} + +var ( + file_pkg_bazel_flags_proto_rawDescOnce sync.Once + file_pkg_bazel_flags_proto_rawDescData = file_pkg_bazel_flags_proto_rawDesc +) + +func file_pkg_bazel_flags_proto_rawDescGZIP() []byte { + file_pkg_bazel_flags_proto_rawDescOnce.Do(func() { + file_pkg_bazel_flags_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_bazel_flags_proto_rawDescData) + }) + return file_pkg_bazel_flags_proto_rawDescData +} + +var file_pkg_bazel_flags_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_pkg_bazel_flags_proto_goTypes = []interface{}{ + (*FlagInfo)(nil), // 0: bazel.FlagInfo + (*FlagCollection)(nil), // 1: bazel.FlagCollection +} +var file_pkg_bazel_flags_proto_depIdxs = []int32{ + 0, // 0: bazel.FlagCollection.flag_infos:type_name -> bazel.FlagInfo + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_pkg_bazel_flags_proto_init() } +func file_pkg_bazel_flags_proto_init() { + if File_pkg_bazel_flags_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_bazel_flags_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FlagInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_bazel_flags_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FlagCollection); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_bazel_flags_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pkg_bazel_flags_proto_goTypes, + DependencyIndexes: file_pkg_bazel_flags_proto_depIdxs, + MessageInfos: file_pkg_bazel_flags_proto_msgTypes, + }.Build() + File_pkg_bazel_flags_proto = out.File + file_pkg_bazel_flags_proto_rawDesc = nil + file_pkg_bazel_flags_proto_goTypes = nil + file_pkg_bazel_flags_proto_depIdxs = nil +}
diff --git a/pkg/bazel/flags.proto b/pkg/bazel/flags.proto new file mode 100644 index 0000000..e8736ae --- /dev/null +++ b/pkg/bazel/flags.proto
@@ -0,0 +1,42 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// This file contains the protocol buffer representation of a list of supported +// flags for Bazel commands. +syntax = "proto2"; + +option java_outer_classname = "BazelFlagsProto"; +option java_package = "com.google.devtools.build.lib.runtime.commands.proto"; +// option java_api_version = 2; + +package bazel; + +message FlagInfo { + // Name of the flag, without leading dashes. + required string name = 1; + // True if --noname exists, too. + optional bool has_negative_flag = 2 [default = false]; + // Help text of the flag. + optional string documentation = 3; + // List of supported Bazel commands, e.g. ['build', 'test'] + repeated string commands = 4; + // Flag name abbreviation, without leading dash. + optional string abbreviation = 5; + // True if a flag is allowed to occur multiple times in a single arg list. + optional bool allows_multiple = 6 [default = false]; +} + +message FlagCollection { + repeated FlagInfo flag_infos = 1; +}