aboutsummaryrefslogtreecommitdiff
path: root/go/tools/gopackagesdriver
diff options
context:
space:
mode:
Diffstat (limited to 'go/tools/gopackagesdriver')
-rw-r--r--go/tools/gopackagesdriver/BUILD.bazel39
-rw-r--r--go/tools/gopackagesdriver/aspect.bzl169
-rw-r--r--go/tools/gopackagesdriver/bazel.go164
-rw-r--r--go/tools/gopackagesdriver/bazel_json_builder.go250
-rw-r--r--go/tools/gopackagesdriver/build_context.go34
-rw-r--r--go/tools/gopackagesdriver/driver_request.go91
-rw-r--r--go/tools/gopackagesdriver/flatpackage.go159
-rw-r--r--go/tools/gopackagesdriver/json_packages_driver.go59
-rw-r--r--go/tools/gopackagesdriver/main.go126
-rw-r--r--go/tools/gopackagesdriver/packageregistry.go111
-rw-r--r--go/tools/gopackagesdriver/utils.go77
11 files changed, 1279 insertions, 0 deletions
diff --git a/go/tools/gopackagesdriver/BUILD.bazel b/go/tools/gopackagesdriver/BUILD.bazel
new file mode 100644
index 00000000..542b75ad
--- /dev/null
+++ b/go/tools/gopackagesdriver/BUILD.bazel
@@ -0,0 +1,39 @@
+load("//go:def.bzl", "go_binary", "go_library")
+load(":aspect.bzl", "bazel_supports_canonical_label_literals")
+
+go_library(
+ name = "gopackagesdriver_lib",
+ srcs = [
+ "bazel.go",
+ "bazel_json_builder.go",
+ "build_context.go",
+ "driver_request.go",
+ "flatpackage.go",
+ "json_packages_driver.go",
+ "main.go",
+ "packageregistry.go",
+ "utils.go",
+ ],
+ importpath = "github.com/bazelbuild/rules_go/go/tools/gopackagesdriver",
+ visibility = ["//visibility:private"],
+)
+
+go_binary(
+ name = "gopackagesdriver",
+ embed = [":gopackagesdriver_lib"],
+ x_defs = {
+ # Determine the name of the rules_go repository as we need to specify it when invoking the
+ # aspect.
+ # If canonical label literals are supported, we can use a canonical label literal (starting
+ # with @@) to pass the repository_name() through repo mapping unchanged.
+ # If canonical label literals are not supported, then bzlmod is certainly not enabled and
+ # we can assume that the repository name is not affected by repo mappings.
+ # If run in the rules_go repo itself, repository_name() returns "@", which is equivalent to
+ # "@io_bazel_rules_go" since Bazel 6:
+ # https://github.com/bazelbuild/bazel/commit/7694cf75e6366b92e3905c2ad60234cda57627ee
+ # TODO: Once we drop support for Bazel 5, we can remove the feature detection logic and
+ # use "@" + repository_name().
+ "rulesGoRepositoryName": "@" + repository_name() if bazel_supports_canonical_label_literals() else repository_name(),
+ },
+ visibility = ["//visibility:public"],
+)
diff --git a/go/tools/gopackagesdriver/aspect.bzl b/go/tools/gopackagesdriver/aspect.bzl
new file mode 100644
index 00000000..36703c75
--- /dev/null
+++ b/go/tools/gopackagesdriver/aspect.bzl
@@ -0,0 +1,169 @@
+# Copyright 2021 The Bazel Go Rules 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.
+
+load(
+ "//go/private:providers.bzl",
+ "GoArchive",
+ "GoStdLib",
+)
+load(
+ "@bazel_skylib//lib:paths.bzl",
+ "paths",
+)
+
+GoPkgInfo = provider()
+
+DEPS_ATTRS = [
+ "deps",
+ "embed",
+]
+
+PROTO_COMPILER_ATTRS = [
+ "compiler",
+ "compilers",
+ "library",
+]
+
+def bazel_supports_canonical_label_literals():
+ return str(Label("//:bogus")).startswith("@@")
+
+def is_file_external(f):
+ return f.owner.workspace_root != ""
+
+def file_path(f):
+ prefix = "__BAZEL_WORKSPACE__"
+ if not f.is_source:
+ prefix = "__BAZEL_EXECROOT__"
+ elif is_file_external(f):
+ prefix = "__BAZEL_OUTPUT_BASE__"
+ return paths.join(prefix, f.path)
+
+def _go_archive_to_pkg(archive):
+ return struct(
+ ID = str(archive.data.label),
+ PkgPath = archive.data.importpath,
+ ExportFile = file_path(archive.data.export_file),
+ GoFiles = [
+ file_path(src)
+ for src in archive.data.orig_srcs
+ if src.path.endswith(".go")
+ ],
+ CompiledGoFiles = [
+ file_path(src)
+ for src in archive.data.srcs
+ if src.path.endswith(".go")
+ ],
+ OtherFiles = [
+ file_path(src)
+ for src in archive.data.orig_srcs
+ if not src.path.endswith(".go")
+ ],
+ Imports = {
+ pkg.data.importpath: str(pkg.data.label)
+ for pkg in archive.direct
+ },
+ )
+
+def make_pkg_json(ctx, name, pkg_info):
+ pkg_json_file = ctx.actions.declare_file(name + ".pkg.json")
+ ctx.actions.write(pkg_json_file, content = pkg_info.to_json())
+ return pkg_json_file
+
+def _go_pkg_info_aspect_impl(target, ctx):
+ # Fetch the stdlib JSON file from the inner most target
+ stdlib_json_file = None
+
+ transitive_json_files = []
+ transitive_export_files = []
+ transitive_compiled_go_files = []
+
+ for attr in DEPS_ATTRS + PROTO_COMPILER_ATTRS:
+ deps = getattr(ctx.rule.attr, attr, []) or []
+
+ # Some attrs are not iterable, ensure that deps is always iterable.
+ if type(deps) != type([]):
+ deps = [deps]
+
+ for dep in deps:
+ if GoPkgInfo in dep:
+ pkg_info = dep[GoPkgInfo]
+ transitive_json_files.append(pkg_info.pkg_json_files)
+ transitive_compiled_go_files.append(pkg_info.compiled_go_files)
+ transitive_export_files.append(pkg_info.export_files)
+
+ # Fetch the stdlib json from the first dependency
+ if not stdlib_json_file:
+ stdlib_json_file = pkg_info.stdlib_json_file
+
+ pkg_json_files = []
+ compiled_go_files = []
+ export_files = []
+
+ if GoArchive in target:
+ archive = target[GoArchive]
+ compiled_go_files.extend(archive.source.srcs)
+ export_files.append(archive.data.export_file)
+ pkg = _go_archive_to_pkg(archive)
+ pkg_json_files.append(make_pkg_json(ctx, archive.data.name, pkg))
+
+ if ctx.rule.kind == "go_test":
+ for dep_archive in archive.direct:
+ # find the archive containing the test sources
+ if archive.data.label == dep_archive.data.label:
+ pkg = _go_archive_to_pkg(dep_archive)
+ pkg_json_files.append(make_pkg_json(ctx, dep_archive.data.name, pkg))
+ compiled_go_files.extend(dep_archive.source.srcs)
+ export_files.append(dep_archive.data.export_file)
+ break
+
+ # If there was no stdlib json in any dependencies, fetch it from the
+ # current go_ node.
+ if not stdlib_json_file:
+ stdlib_json_file = ctx.attr._go_stdlib[GoStdLib]._list_json
+
+ pkg_info = GoPkgInfo(
+ stdlib_json_file = stdlib_json_file,
+ pkg_json_files = depset(
+ direct = pkg_json_files,
+ transitive = transitive_json_files,
+ ),
+ compiled_go_files = depset(
+ direct = compiled_go_files,
+ transitive = transitive_compiled_go_files,
+ ),
+ export_files = depset(
+ direct = export_files,
+ transitive = transitive_export_files,
+ ),
+ )
+
+ return [
+ pkg_info,
+ OutputGroupInfo(
+ go_pkg_driver_json_file = pkg_info.pkg_json_files,
+ go_pkg_driver_srcs = pkg_info.compiled_go_files,
+ go_pkg_driver_export_file = pkg_info.export_files,
+ go_pkg_driver_stdlib_json_file = depset([pkg_info.stdlib_json_file] if pkg_info.stdlib_json_file else []),
+ ),
+ ]
+
+go_pkg_info_aspect = aspect(
+ implementation = _go_pkg_info_aspect_impl,
+ attr_aspects = DEPS_ATTRS + PROTO_COMPILER_ATTRS,
+ attrs = {
+ "_go_stdlib": attr.label(
+ default = "//:stdlib",
+ ),
+ },
+)
diff --git a/go/tools/gopackagesdriver/bazel.go b/go/tools/gopackagesdriver/bazel.go
new file mode 100644
index 00000000..08da745d
--- /dev/null
+++ b/go/tools/gopackagesdriver/bazel.go
@@ -0,0 +1,164 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+)
+
+const (
+ toolTag = "gopackagesdriver"
+)
+
+type Bazel struct {
+ bazelBin string
+ workspaceRoot string
+ bazelStartupFlags []string
+ info map[string]string
+}
+
+// Minimal BEP structs to access the build outputs
+type BEPNamedSet struct {
+ NamedSetOfFiles *struct {
+ Files []struct {
+ Name string `json:"name"`
+ URI string `json:"uri"`
+ } `json:"files"`
+ } `json:"namedSetOfFiles"`
+}
+
+func NewBazel(ctx context.Context, bazelBin, workspaceRoot string, bazelStartupFlags []string) (*Bazel, error) {
+ b := &Bazel{
+ bazelBin: bazelBin,
+ workspaceRoot: workspaceRoot,
+ bazelStartupFlags: bazelStartupFlags,
+ }
+ if err := b.fillInfo(ctx); err != nil {
+ return nil, fmt.Errorf("unable to query bazel info: %w", err)
+ }
+ return b, nil
+}
+
+func (b *Bazel) fillInfo(ctx context.Context) error {
+ b.info = map[string]string{}
+ output, err := b.run(ctx, "info")
+ if err != nil {
+ return err
+ }
+ scanner := bufio.NewScanner(bytes.NewBufferString(output))
+ for scanner.Scan() {
+ parts := strings.SplitN(strings.TrimSpace(scanner.Text()), ":", 2)
+ b.info[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
+ }
+ return nil
+}
+
+func (b *Bazel) run(ctx context.Context, command string, args ...string) (string, error) {
+ defaultArgs := []string{
+ command,
+ "--tool_tag=" + toolTag,
+ "--ui_actions_shown=0",
+ }
+ cmd := exec.CommandContext(ctx, b.bazelBin, concatStringsArrays(b.bazelStartupFlags, defaultArgs, args)...)
+ fmt.Fprintln(os.Stderr, "Running:", cmd.Args)
+ cmd.Dir = b.WorkspaceRoot()
+ cmd.Stderr = os.Stderr
+ output, err := cmd.Output()
+ return string(output), err
+}
+
+func (b *Bazel) Build(ctx context.Context, args ...string) ([]string, error) {
+ jsonFile, err := ioutil.TempFile("", "gopackagesdriver_bep_")
+ if err != nil {
+ return nil, fmt.Errorf("unable to create BEP JSON file: %w", err)
+ }
+ defer func() {
+ jsonFile.Close()
+ os.Remove(jsonFile.Name())
+ }()
+
+ args = append([]string{
+ "--show_result=0",
+ "--build_event_json_file=" + jsonFile.Name(),
+ "--build_event_json_file_path_conversion=no",
+ }, args...)
+ if _, err := b.run(ctx, "build", args...); err != nil {
+ // Ignore a regular build failure to get partial data.
+ // See https://docs.bazel.build/versions/main/guide.html#what-exit-code-will-i-get on
+ // exit codes.
+ var exerr *exec.ExitError
+ if !errors.As(err, &exerr) || exerr.ExitCode() != 1 {
+ return nil, fmt.Errorf("bazel build failed: %w", err)
+ }
+ }
+
+ files := make([]string, 0)
+ decoder := json.NewDecoder(jsonFile)
+ for decoder.More() {
+ var namedSet BEPNamedSet
+ if err := decoder.Decode(&namedSet); err != nil {
+ return nil, fmt.Errorf("unable to decode %s: %w", jsonFile.Name(), err)
+ }
+
+ if namedSet.NamedSetOfFiles != nil {
+ for _, f := range namedSet.NamedSetOfFiles.Files {
+ fileUrl, err := url.Parse(f.URI)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse file URI: %w", err)
+ }
+ files = append(files, filepath.FromSlash(fileUrl.Path))
+ }
+ }
+ }
+
+ return files, nil
+}
+
+func (b *Bazel) Query(ctx context.Context, args ...string) ([]string, error) {
+ output, err := b.run(ctx, "query", args...)
+ if err != nil {
+ return nil, fmt.Errorf("bazel query failed: %w", err)
+ }
+
+ trimmedOutput := strings.TrimSpace(output)
+ if len(trimmedOutput) == 0 {
+ return nil, nil
+ }
+
+ return strings.Split(trimmedOutput, "\n"), nil
+}
+
+func (b *Bazel) WorkspaceRoot() string {
+ return b.workspaceRoot
+}
+
+func (b *Bazel) ExecutionRoot() string {
+ return b.info["execution_root"]
+}
+
+func (b *Bazel) OutputBase() string {
+ return b.info["output_base"]
+}
diff --git a/go/tools/gopackagesdriver/bazel_json_builder.go b/go/tools/gopackagesdriver/bazel_json_builder.go
new file mode 100644
index 00000000..163be082
--- /dev/null
+++ b/go/tools/gopackagesdriver/bazel_json_builder.go
@@ -0,0 +1,250 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+ "bufio"
+ "context"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+type BazelJSONBuilder struct {
+ bazel *Bazel
+ includeTests bool
+}
+
+var RulesGoStdlibLabel = rulesGoRepositoryName + "//:stdlib"
+
+var _defaultKinds = []string{"go_library", "go_test", "go_binary"}
+
+var externalRe = regexp.MustCompile(".*\\/external\\/([^\\/]+)(\\/(.*))?\\/([^\\/]+.go)")
+
+func (b *BazelJSONBuilder) fileQuery(filename string) string {
+ label := filename
+
+ if filepath.IsAbs(filename) {
+ label, _ = filepath.Rel(b.bazel.WorkspaceRoot(), filename)
+ } else if strings.HasPrefix(filename, "./") {
+ label = strings.TrimPrefix(filename, "./")
+ }
+
+ if matches := externalRe.FindStringSubmatch(filename); len(matches) == 5 {
+ // if filepath is for a third party lib, we need to know, what external
+ // library this file is part of.
+ matches = append(matches[:2], matches[3:]...)
+ label = fmt.Sprintf("@%s//%s", matches[1], strings.Join(matches[2:], ":"))
+ }
+
+ relToBin, err := filepath.Rel(b.bazel.info["output_path"], filename)
+ if err == nil && !strings.HasPrefix(relToBin, "../") {
+ parts := strings.SplitN(relToBin, string(filepath.Separator), 3)
+ relToBin = parts[2]
+ // We've effectively converted filename from bazel-bin/some/path.go to some/path.go;
+ // Check if a BUILD.bazel files exists under this dir, if not walk up and repeat.
+ relToBin = filepath.Dir(relToBin)
+ _, err = os.Stat(filepath.Join(b.bazel.WorkspaceRoot(), relToBin, "BUILD.bazel"))
+ for errors.Is(err, os.ErrNotExist) && relToBin != "." {
+ relToBin = filepath.Dir(relToBin)
+ _, err = os.Stat(filepath.Join(b.bazel.WorkspaceRoot(), relToBin, "BUILD.bazel"))
+ }
+
+ if err == nil {
+ // return package path found and build all targets (codegen doesn't fall under go_library)
+ // Otherwise fallback to default
+ if relToBin == "." {
+ relToBin = ""
+ }
+ label = fmt.Sprintf("//%s:all", relToBin)
+ additionalKinds = append(additionalKinds, "go_.*")
+ }
+ }
+
+ kinds := append(_defaultKinds, additionalKinds...)
+ return fmt.Sprintf(`kind("%s", same_pkg_direct_rdeps("%s"))`, strings.Join(kinds, "|"), label)
+}
+
+func (b *BazelJSONBuilder) getKind() string {
+ kinds := []string{"go_library"}
+ if b.includeTests {
+ kinds = append(kinds, "go_test")
+ }
+
+ return strings.Join(kinds, "|")
+}
+
+func (b *BazelJSONBuilder) localQuery(request string) string {
+ request = path.Clean(request)
+ if filepath.IsAbs(request) {
+ if relPath, err := filepath.Rel(workspaceRoot, request); err == nil {
+ request = relPath
+ }
+ }
+
+ if !strings.HasSuffix(request, "...") {
+ request = fmt.Sprintf("%s:*", request)
+ }
+
+ return fmt.Sprintf(`kind("%s", %s)`, b.getKind(), request)
+}
+
+func (b *BazelJSONBuilder) packageQuery(importPath string) string {
+ if strings.HasSuffix(importPath, "/...") {
+ importPath = fmt.Sprintf(`^%s(/.+)?$`, strings.TrimSuffix(importPath, "/..."))
+ }
+
+ return fmt.Sprintf(
+ `kind("%s", attr(importpath, "%s", deps(%s)))`,
+ b.getKind(),
+ importPath,
+ bazelQueryScope)
+}
+
+func (b *BazelJSONBuilder) queryFromRequests(requests ...string) string {
+ ret := make([]string, 0, len(requests))
+ for _, request := range requests {
+ result := ""
+ if strings.HasSuffix(request, ".go") {
+ f := strings.TrimPrefix(request, "file=")
+ result = b.fileQuery(f)
+ } else if isLocalPattern(request) {
+ result = b.localQuery(request)
+ } else if request == "builtin" || request == "std" {
+ result = fmt.Sprintf(RulesGoStdlibLabel)
+ } else if bazelQueryScope != "" {
+ result = b.packageQuery(request)
+ }
+
+ if result != "" {
+ ret = append(ret, result)
+ }
+ }
+ if len(ret) == 0 {
+ return RulesGoStdlibLabel
+ }
+ return strings.Join(ret, " union ")
+}
+
+func NewBazelJSONBuilder(bazel *Bazel, includeTests bool) (*BazelJSONBuilder, error) {
+ return &BazelJSONBuilder{
+ bazel: bazel,
+ includeTests: includeTests,
+ }, nil
+}
+
+func (b *BazelJSONBuilder) outputGroupsForMode(mode LoadMode) string {
+ og := "go_pkg_driver_json_file,go_pkg_driver_stdlib_json_file,go_pkg_driver_srcs"
+ if mode&NeedExportsFile != 0 {
+ og += ",go_pkg_driver_export_file"
+ }
+ return og
+}
+
+func (b *BazelJSONBuilder) query(ctx context.Context, query string) ([]string, error) {
+ queryArgs := concatStringsArrays(bazelQueryFlags, []string{
+ "--ui_event_filters=-info,-stderr",
+ "--noshow_progress",
+ "--order_output=no",
+ "--output=label",
+ "--nodep_deps",
+ "--noimplicit_deps",
+ "--notool_deps",
+ query,
+ })
+ labels, err := b.bazel.Query(ctx, queryArgs...)
+ if err != nil {
+ return nil, fmt.Errorf("unable to query: %w", err)
+ }
+
+ return labels, nil
+}
+
+func (b *BazelJSONBuilder) Labels(ctx context.Context, requests []string) ([]string, error) {
+ labels, err := b.query(ctx, b.queryFromRequests(requests...))
+ if err != nil {
+ return nil, fmt.Errorf("query failed: %w", err)
+ }
+
+ if len(labels) == 0 {
+ return nil, fmt.Errorf("found no labels matching the requests")
+ }
+
+ return labels, nil
+}
+
+func (b *BazelJSONBuilder) Build(ctx context.Context, labels []string, mode LoadMode) ([]string, error) {
+ aspects := append(additionalAspects, goDefaultAspect)
+
+ buildArgs := concatStringsArrays([]string{
+ "--experimental_convenience_symlinks=ignore",
+ "--ui_event_filters=-info,-stderr",
+ "--noshow_progress",
+ "--aspects=" + strings.Join(aspects, ","),
+ "--output_groups=" + b.outputGroupsForMode(mode),
+ "--keep_going", // Build all possible packages
+ }, bazelBuildFlags)
+
+ if len(labels) < 100 {
+ buildArgs = append(buildArgs, labels...)
+ } else {
+ // To avoid hitting MAX_ARGS length, write labels to a file and use `--target_pattern_file`
+ targetsFile, err := ioutil.TempFile("", "gopackagesdriver_targets_")
+ if err != nil {
+ return nil, fmt.Errorf("unable to create target pattern file: %w", err)
+ }
+ writer := bufio.NewWriter(targetsFile)
+ defer writer.Flush()
+ for _, l := range labels {
+ writer.WriteString(l + "\n")
+ }
+ if err := writer.Flush(); err != nil {
+ return nil, fmt.Errorf("unable to flush data to target pattern file: %w", err)
+ }
+ defer func() {
+ targetsFile.Close()
+ os.Remove(targetsFile.Name())
+ }()
+
+ buildArgs = append(buildArgs, "--target_pattern_file="+targetsFile.Name())
+ }
+ files, err := b.bazel.Build(ctx, buildArgs...)
+ if err != nil {
+ return nil, fmt.Errorf("unable to bazel build %v: %w", buildArgs, err)
+ }
+
+ ret := []string{}
+ for _, f := range files {
+ if strings.HasSuffix(f, ".pkg.json") {
+ ret = append(ret, f)
+ }
+ }
+
+ return ret, nil
+}
+
+func (b *BazelJSONBuilder) PathResolver() PathResolverFunc {
+ return func(p string) string {
+ p = strings.Replace(p, "__BAZEL_EXECROOT__", b.bazel.ExecutionRoot(), 1)
+ p = strings.Replace(p, "__BAZEL_WORKSPACE__", b.bazel.WorkspaceRoot(), 1)
+ p = strings.Replace(p, "__BAZEL_OUTPUT_BASE__", b.bazel.OutputBase(), 1)
+ return p
+ }
+}
diff --git a/go/tools/gopackagesdriver/build_context.go b/go/tools/gopackagesdriver/build_context.go
new file mode 100644
index 00000000..dac786b9
--- /dev/null
+++ b/go/tools/gopackagesdriver/build_context.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "go/build"
+ "path/filepath"
+ "strings"
+)
+
+var buildContext = makeBuildContext()
+
+func makeBuildContext() *build.Context {
+ bctx := build.Default
+ bctx.BuildTags = strings.Split(getenvDefault("GOTAGS", ""), ",")
+
+ return &bctx
+}
+
+func filterSourceFilesForTags(files []string) []string {
+ ret := make([]string, 0, len(files))
+
+ for _, f := range files {
+ dir, filename := filepath.Split(f)
+ ext := filepath.Ext(f)
+
+ match, _ := buildContext.MatchFile(dir, filename)
+ // MatchFile filters out anything without a file extension. In the
+ // case of CompiledGoFiles (in particular gco processed files from
+ // the cache), we want them.
+ if match || ext == "" {
+ ret = append(ret, f)
+ }
+ }
+ return ret
+}
diff --git a/go/tools/gopackagesdriver/driver_request.go b/go/tools/gopackagesdriver/driver_request.go
new file mode 100644
index 00000000..db572dcc
--- /dev/null
+++ b/go/tools/gopackagesdriver/driver_request.go
@@ -0,0 +1,91 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+)
+
+// From https://pkg.go.dev/golang.org/x/tools/go/packages#LoadMode
+type LoadMode int
+
+// Only NeedExportsFile is needed in our case
+const (
+ // NeedName adds Name and PkgPath.
+ NeedName LoadMode = 1 << iota
+
+ // NeedFiles adds GoFiles and OtherFiles.
+ NeedFiles
+
+ // NeedCompiledGoFiles adds CompiledGoFiles.
+ NeedCompiledGoFiles
+
+ // NeedImports adds Imports. If NeedDeps is not set, the Imports field will contain
+ // "placeholder" Packages with only the ID set.
+ NeedImports
+
+ // NeedDeps adds the fields requested by the LoadMode in the packages in Imports.
+ NeedDeps
+
+ // NeedExportsFile adds ExportFile.
+ NeedExportFile
+
+ // NeedTypes adds Types, Fset, and IllTyped.
+ NeedTypes
+
+ // NeedSyntax adds Syntax.
+ NeedSyntax
+
+ // NeedTypesInfo adds TypesInfo.
+ NeedTypesInfo
+
+ // NeedTypesSizes adds TypesSizes.
+ NeedTypesSizes
+
+ // typecheckCgo enables full support for type checking cgo. Requires Go 1.15+.
+ // Modifies CompiledGoFiles and Types, and has no effect on its own.
+ typecheckCgo
+
+ // NeedModule adds Module.
+ NeedModule
+)
+
+// Deprecated: NeedExportsFile is a historical misspelling of NeedExportFile.
+const NeedExportsFile = NeedExportFile
+
+// From https://github.com/golang/tools/blob/v0.1.0/go/packages/external.go#L32
+// Most fields are disabled since there is no need for them
+type DriverRequest struct {
+ Mode LoadMode `json:"mode"`
+ // Env specifies the environment the underlying build system should be run in.
+ // Env []string `json:"env"`
+ // BuildFlags are flags that should be passed to the underlying build system.
+ // BuildFlags []string `json:"build_flags"`
+ // Tests specifies whether the patterns should also return test packages.
+ Tests bool `json:"tests"`
+ // Overlay maps file paths (relative to the driver's working directory) to the byte contents
+ // of overlay files.
+ // Overlay map[string][]byte `json:"overlay"`
+}
+
+func ReadDriverRequest(r io.Reader) (*DriverRequest, error) {
+ req := &DriverRequest{}
+ if err := json.NewDecoder(r).Decode(&req); err != nil {
+ return nil, fmt.Errorf("unable to decode driver request: %w", err)
+ }
+ return req, nil
+}
diff --git a/go/tools/gopackagesdriver/flatpackage.go b/go/tools/gopackagesdriver/flatpackage.go
new file mode 100644
index 00000000..9c22132a
--- /dev/null
+++ b/go/tools/gopackagesdriver/flatpackage.go
@@ -0,0 +1,159 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "go/parser"
+ "go/token"
+ "os"
+ "strconv"
+ "strings"
+)
+
+type ResolvePkgFunc func(importPath string) string
+
+// Copy and pasted from golang.org/x/tools/go/packages
+type FlatPackagesError struct {
+ Pos string // "file:line:col" or "file:line" or "" or "-"
+ Msg string
+ Kind FlatPackagesErrorKind
+}
+
+type FlatPackagesErrorKind int
+
+const (
+ UnknownError FlatPackagesErrorKind = iota
+ ListError
+ ParseError
+ TypeError
+)
+
+func (err FlatPackagesError) Error() string {
+ pos := err.Pos
+ if pos == "" {
+ pos = "-" // like token.Position{}.String()
+ }
+ return pos + ": " + err.Msg
+}
+
+// FlatPackage is the JSON form of Package
+// It drops all the type and syntax fields, and transforms the Imports
+type FlatPackage struct {
+ ID string
+ Name string `json:",omitempty"`
+ PkgPath string `json:",omitempty"`
+ Errors []FlatPackagesError `json:",omitempty"`
+ GoFiles []string `json:",omitempty"`
+ CompiledGoFiles []string `json:",omitempty"`
+ OtherFiles []string `json:",omitempty"`
+ ExportFile string `json:",omitempty"`
+ Imports map[string]string `json:",omitempty"`
+ Standard bool `json:",omitempty"`
+}
+
+type (
+ PackageFunc func(pkg *FlatPackage)
+ PathResolverFunc func(path string) string
+)
+
+func resolvePathsInPlace(prf PathResolverFunc, paths []string) {
+ for i, path := range paths {
+ paths[i] = prf(path)
+ }
+}
+
+func WalkFlatPackagesFromJSON(jsonFile string, onPkg PackageFunc) error {
+ f, err := os.Open(jsonFile)
+ if err != nil {
+ return fmt.Errorf("unable to open package JSON file: %w", err)
+ }
+ defer f.Close()
+
+ decoder := json.NewDecoder(f)
+ for decoder.More() {
+ pkg := &FlatPackage{}
+ if err := decoder.Decode(&pkg); err != nil {
+ return fmt.Errorf("unable to decode package in %s: %w", f.Name(), err)
+ }
+
+ onPkg(pkg)
+ }
+ return nil
+}
+
+func (fp *FlatPackage) ResolvePaths(prf PathResolverFunc) error {
+ resolvePathsInPlace(prf, fp.CompiledGoFiles)
+ resolvePathsInPlace(prf, fp.GoFiles)
+ resolvePathsInPlace(prf, fp.OtherFiles)
+ fp.ExportFile = prf(fp.ExportFile)
+ return nil
+}
+
+// FilterFilesForBuildTags filters the source files given the current build
+// tags.
+func (fp *FlatPackage) FilterFilesForBuildTags() {
+ fp.GoFiles = filterSourceFilesForTags(fp.GoFiles)
+ fp.CompiledGoFiles = filterSourceFilesForTags(fp.CompiledGoFiles)
+}
+
+func (fp *FlatPackage) IsStdlib() bool {
+ return fp.Standard
+}
+
+func (fp *FlatPackage) ResolveImports(resolve ResolvePkgFunc) error {
+ // Stdlib packages are already complete import wise
+ if fp.IsStdlib() {
+ return nil
+ }
+
+ fset := token.NewFileSet()
+
+ for _, file := range fp.CompiledGoFiles {
+ f, err := parser.ParseFile(fset, file, nil, parser.ImportsOnly)
+ if err != nil {
+ return err
+ }
+ // If the name is not provided, fetch it from the sources
+ if fp.Name == "" {
+ fp.Name = f.Name.Name
+ }
+
+ for _, rawImport := range f.Imports {
+ imp, err := strconv.Unquote(rawImport.Path.Value)
+ if err != nil {
+ continue
+ }
+ // We don't handle CGo for now
+ if imp == "C" {
+ continue
+ }
+ if _, ok := fp.Imports[imp]; ok {
+ continue
+ }
+
+ if pkgID := resolve(imp); pkgID != "" {
+ fp.Imports[imp] = pkgID
+ }
+ }
+ }
+
+ return nil
+}
+
+func (fp *FlatPackage) IsRoot() bool {
+ return strings.HasPrefix(fp.ID, "//")
+}
diff --git a/go/tools/gopackagesdriver/json_packages_driver.go b/go/tools/gopackagesdriver/json_packages_driver.go
new file mode 100644
index 00000000..9bbf3408
--- /dev/null
+++ b/go/tools/gopackagesdriver/json_packages_driver.go
@@ -0,0 +1,59 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+ "fmt"
+ "go/types"
+)
+
+type JSONPackagesDriver struct {
+ registry *PackageRegistry
+}
+
+func NewJSONPackagesDriver(jsonFiles []string, prf PathResolverFunc) (*JSONPackagesDriver, error) {
+ jpd := &JSONPackagesDriver{
+ registry: NewPackageRegistry(),
+ }
+
+ for _, f := range jsonFiles {
+ if err := WalkFlatPackagesFromJSON(f, func(pkg *FlatPackage) {
+ jpd.registry.Add(pkg)
+ }); err != nil {
+ return nil, fmt.Errorf("unable to walk json: %w", err)
+ }
+ }
+
+ if err := jpd.registry.ResolvePaths(prf); err != nil {
+ return nil, fmt.Errorf("unable to resolve paths: %w", err)
+ }
+
+ if err := jpd.registry.ResolveImports(); err != nil {
+ return nil, fmt.Errorf("unable to resolve paths: %w", err)
+ }
+
+ return jpd, nil
+}
+
+func (b *JSONPackagesDriver) GetResponse(labels []string) *driverResponse {
+ rootPkgs, packages := b.registry.Match(labels)
+
+ return &driverResponse{
+ NotHandled: false,
+ Sizes: types.SizesFor("gc", "amd64").(*types.StdSizes),
+ Roots: rootPkgs,
+ Packages: packages,
+ }
+}
diff --git a/go/tools/gopackagesdriver/main.go b/go/tools/gopackagesdriver/main.go
new file mode 100644
index 00000000..fea2d2c1
--- /dev/null
+++ b/go/tools/gopackagesdriver/main.go
@@ -0,0 +1,126 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "go/types"
+ "os"
+ "strings"
+)
+
+type driverResponse struct {
+ // NotHandled is returned if the request can't be handled by the current
+ // driver. If an external driver returns a response with NotHandled, the
+ // rest of the driverResponse is ignored, and go/packages will fallback
+ // to the next driver. If go/packages is extended in the future to support
+ // lists of multiple drivers, go/packages will fall back to the next driver.
+ NotHandled bool
+
+ // Sizes, if not nil, is the types.Sizes to use when type checking.
+ Sizes *types.StdSizes
+
+ // Roots is the set of package IDs that make up the root packages.
+ // We have to encode this separately because when we encode a single package
+ // we cannot know if it is one of the roots as that requires knowledge of the
+ // graph it is part of.
+ Roots []string `json:",omitempty"`
+
+ // Packages is the full set of packages in the graph.
+ // The packages are not connected into a graph.
+ // The Imports if populated will be stubs that only have their ID set.
+ // Imports will be connected and then type and syntax information added in a
+ // later pass (see refine).
+ Packages []*FlatPackage
+}
+
+var (
+ // Injected via x_defs.
+
+ rulesGoRepositoryName string
+ goDefaultAspect = rulesGoRepositoryName + "//go/tools/gopackagesdriver:aspect.bzl%go_pkg_info_aspect"
+ bazelBin = getenvDefault("GOPACKAGESDRIVER_BAZEL", "bazel")
+ bazelStartupFlags = strings.Fields(os.Getenv("GOPACKAGESDRIVER_BAZEL_FLAGS"))
+ bazelQueryFlags = strings.Fields(os.Getenv("GOPACKAGESDRIVER_BAZEL_QUERY_FLAGS"))
+ bazelQueryScope = getenvDefault("GOPACKAGESDRIVER_BAZEL_QUERY_SCOPE", "")
+ bazelBuildFlags = strings.Fields(os.Getenv("GOPACKAGESDRIVER_BAZEL_BUILD_FLAGS"))
+ workspaceRoot = os.Getenv("BUILD_WORKSPACE_DIRECTORY")
+ additionalAspects = strings.Fields(os.Getenv("GOPACKAGESDRIVER_BAZEL_ADDTL_ASPECTS"))
+ additionalKinds = strings.Fields(os.Getenv("GOPACKAGESDRIVER_BAZEL_KINDS"))
+ emptyResponse = &driverResponse{
+ NotHandled: true,
+ Sizes: types.SizesFor("gc", "amd64").(*types.StdSizes),
+ Roots: []string{},
+ Packages: []*FlatPackage{},
+ }
+)
+
+func run() (*driverResponse, error) {
+ ctx, cancel := signalContext(context.Background(), os.Interrupt)
+ defer cancel()
+
+ queries := os.Args[1:]
+
+ request, err := ReadDriverRequest(os.Stdin)
+ if err != nil {
+ return emptyResponse, fmt.Errorf("unable to read request: %w", err)
+ }
+
+ bazel, err := NewBazel(ctx, bazelBin, workspaceRoot, bazelStartupFlags)
+ if err != nil {
+ return emptyResponse, fmt.Errorf("unable to create bazel instance: %w", err)
+ }
+
+ bazelJsonBuilder, err := NewBazelJSONBuilder(bazel, request.Tests)
+ if err != nil {
+ return emptyResponse, fmt.Errorf("unable to build JSON files: %w", err)
+ }
+
+ labels, err := bazelJsonBuilder.Labels(ctx, queries)
+ if err != nil {
+ return emptyResponse, fmt.Errorf("unable to lookup package: %w", err)
+ }
+
+ jsonFiles, err := bazelJsonBuilder.Build(ctx, labels, request.Mode)
+ if err != nil {
+ return emptyResponse, fmt.Errorf("unable to build JSON files: %w", err)
+ }
+
+ driver, err := NewJSONPackagesDriver(jsonFiles, bazelJsonBuilder.PathResolver())
+ if err != nil {
+ return emptyResponse, fmt.Errorf("unable to load JSON files: %w", err)
+ }
+
+ // Note: we are returning all files required to build a specific package.
+ // For file queries (`file=`), this means that the CompiledGoFiles will
+ // include more than the only file being specified.
+ return driver.GetResponse(labels), nil
+}
+
+func main() {
+ response, err := run()
+ if err := json.NewEncoder(os.Stdout).Encode(response); err != nil {
+ fmt.Fprintf(os.Stderr, "unable to encode response: %v", err)
+ }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error: %v", err)
+ // gopls will check the packages driver exit code, and if there is an
+ // error, it will fall back to go list. Obviously we don't want that,
+ // so force a 0 exit code.
+ os.Exit(0)
+ }
+}
diff --git a/go/tools/gopackagesdriver/packageregistry.go b/go/tools/gopackagesdriver/packageregistry.go
new file mode 100644
index 00000000..05e620d5
--- /dev/null
+++ b/go/tools/gopackagesdriver/packageregistry.go
@@ -0,0 +1,111 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+ "fmt"
+ "strings"
+)
+
+type PackageRegistry struct {
+ packagesByID map[string]*FlatPackage
+ stdlib map[string]string
+}
+
+func NewPackageRegistry(pkgs ...*FlatPackage) *PackageRegistry {
+ pr := &PackageRegistry{
+ packagesByID: map[string]*FlatPackage{},
+ stdlib: map[string]string{},
+ }
+ pr.Add(pkgs...)
+ return pr
+}
+
+func (pr *PackageRegistry) Add(pkgs ...*FlatPackage) *PackageRegistry {
+ for _, pkg := range pkgs {
+ pr.packagesByID[pkg.ID] = pkg
+
+ if pkg.IsStdlib() {
+ pr.stdlib[pkg.PkgPath] = pkg.ID
+ }
+ }
+ return pr
+}
+
+func (pr *PackageRegistry) ResolvePaths(prf PathResolverFunc) error {
+ for _, pkg := range pr.packagesByID {
+ pkg.ResolvePaths(prf)
+ pkg.FilterFilesForBuildTags()
+ }
+ return nil
+}
+
+// ResolveImports adds stdlib imports to packages. This is required because
+// stdlib packages are not part of the JSON file exports as bazel is unaware of
+// them.
+func (pr *PackageRegistry) ResolveImports() error {
+ resolve := func(importPath string) string {
+ if pkgID, ok := pr.stdlib[importPath]; ok {
+ return pkgID
+ }
+
+ return ""
+ }
+
+ for _, pkg := range pr.packagesByID {
+ if err := pkg.ResolveImports(resolve); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (pr *PackageRegistry) walk(acc map[string]*FlatPackage, root string) {
+ pkg := pr.packagesByID[root]
+
+ acc[pkg.ID] = pkg
+ for _, pkgID := range pkg.Imports {
+ if _, ok := acc[pkgID]; !ok {
+ pr.walk(acc, pkgID)
+ }
+ }
+}
+
+func (pr *PackageRegistry) Match(labels []string) ([]string, []*FlatPackage) {
+ roots := map[string]struct{}{}
+
+ for _, label := range labels {
+ if !strings.HasPrefix(label, "@") {
+ label = fmt.Sprintf("@%s", label)
+ }
+
+ roots[label] = struct{}{}
+ }
+
+ walkedPackages := map[string]*FlatPackage{}
+ retRoots := make([]string, 0, len(roots))
+ for rootPkg := range roots {
+ retRoots = append(retRoots, rootPkg)
+ pr.walk(walkedPackages, rootPkg)
+ }
+
+ retPkgs := make([]*FlatPackage, 0, len(walkedPackages))
+ for _, pkg := range walkedPackages {
+ retPkgs = append(retPkgs, pkg)
+ }
+
+ return retRoots, retPkgs
+}
diff --git a/go/tools/gopackagesdriver/utils.go b/go/tools/gopackagesdriver/utils.go
new file mode 100644
index 00000000..d5524fdd
--- /dev/null
+++ b/go/tools/gopackagesdriver/utils.go
@@ -0,0 +1,77 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "go/build"
+ "os"
+ "os/signal"
+ "path"
+ "path/filepath"
+)
+
+func getenvDefault(key, defaultValue string) string {
+ if v, ok := os.LookupEnv(key); ok {
+ return v
+ }
+ return defaultValue
+}
+
+func concatStringsArrays(values ...[]string) []string {
+ ret := []string{}
+ for _, v := range values {
+ ret = append(ret, v...)
+ }
+ return ret
+}
+
+func ensureAbsolutePathFromWorkspace(path string) string {
+ if filepath.IsAbs(path) {
+ return path
+ }
+ return filepath.Join(workspaceRoot, path)
+}
+
+func signalContext(parentCtx context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
+ ctx, cancel := context.WithCancel(parentCtx)
+ ch := make(chan os.Signal, 1)
+ go func() {
+ select {
+ case <-ch:
+ cancel()
+ case <-ctx.Done():
+ }
+ }()
+ signal.Notify(ch, signals...)
+
+ return ctx, cancel
+}
+
+func isLocalPattern(pattern string) bool {
+ return build.IsLocalImport(pattern) || filepath.IsAbs(pattern)
+}
+
+func packageID(pattern string) string {
+ pattern = path.Clean(pattern)
+ if filepath.IsAbs(pattern) {
+ if relPath, err := filepath.Rel(workspaceRoot, pattern); err == nil {
+ pattern = relPath
+ }
+ }
+
+ return fmt.Sprintf("//%s", pattern)
+}