diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..7b30843919 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "bazelbuild.vscode-bazel", + "golang.go", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..d81fc004ef --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "go.goroot": "${workspaceFolder}/bazel-${workspaceFolderBasename}/external/go_sdk", + "go.toolsEnvVars": { + "GOPACKAGESDRIVER": "${workspaceFolder}/tools/gopackagesdriver.sh" + }, + "go.enableCodeLens": { + "references": false, + "runtest": false + }, + "gopls": { + "formatting.gofumpt": true, + "formatting.local": "github.com/bazelbuild/rules_go", + "ui.completion.usePlaceholders": true, + "ui.semanticTokens": true, + "ui.codelenses": { + "gc_details": false, + "regenerate_cgo": false, + "generate": false, + "test": false, + "tidy": false, + "upgrade_dependency": false, + "vendor": false + }, + }, + "go.useLanguageServer": true, + "go.buildOnSave": "off", + "go.lintOnSave": "off", + "go.vetOnSave": "off", +} diff --git a/go/tools/builders/stdliblist.go b/go/tools/builders/stdliblist.go index 90564a2d3e..ec7e3161ae 100644 --- a/go/tools/builders/stdliblist.go +++ b/go/tools/builders/stdliblist.go @@ -111,7 +111,7 @@ func stdlibPackageID(importPath string) string { func execRootPath(execRoot, p string) string { dir, _ := filepath.Rel(execRoot, p) - return filepath.Join("__BAZEL_EXECROOT__", dir) + return filepath.Join("__BAZEL_OUTPUT_BASE__", dir) } func absoluteSourcesPaths(execRoot, pkgDir string, srcs []string) []string { diff --git a/go/tools/gopackagesdriver/BUILD.bazel b/go/tools/gopackagesdriver/BUILD.bazel index 5c7ee3369b..5025cf6379 100644 --- a/go/tools/gopackagesdriver/BUILD.bazel +++ b/go/tools/gopackagesdriver/BUILD.bazel @@ -5,11 +5,13 @@ go_library( 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"], diff --git a/go/tools/gopackagesdriver/aspect.bzl b/go/tools/gopackagesdriver/aspect.bzl index 3a15d96b9b..de9eacd227 100644 --- a/go/tools/gopackagesdriver/aspect.bzl +++ b/go/tools/gopackagesdriver/aspect.bzl @@ -17,18 +17,10 @@ load( "GoArchive", "GoStdLib", ) -load( - "//go/private:context.bzl", - "go_context", -) load( "@bazel_skylib//lib:paths.bzl", "paths", ) -load( - "@bazel_skylib//lib:collections.bzl", - "collections", -) GoPkgInfo = provider() @@ -36,9 +28,32 @@ def _is_file_external(f): return f.owner.workspace_root != "" def _file_path(f): - if f.is_source and not _is_file_external(f): - return paths.join("__BAZEL_WORKSPACE__", f.path) - return paths.join("__BAZEL_EXECROOT__", f.path) + 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 + ], + CompiledGoFiles = [ + _file_path(src) + for src in archive.data.srcs + ], + ) + +def _make_pkg_json(ctx, archive, pkg_info): + pkg_json_file = ctx.actions.declare_file(archive.data.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 @@ -46,61 +61,79 @@ def _go_pkg_info_aspect_impl(target, ctx): deps_transitive_json_file = [] deps_transitive_export_file = [] - for dep in getattr(ctx.rule.attr, "deps", []): - if GoPkgInfo in dep: - pkg_info = dep[GoPkgInfo] - deps_transitive_json_file.append(pkg_info.transitive_json_file) - deps_transitive_export_file.append(pkg_info.transitive_export_file) - # Fetch the stdlib json from the first dependency - if not stdlib_json_file: - stdlib_json_file = pkg_info.stdlib_json_file - - # If deps are embedded, do not gather their json or export_file since they - # are included in the current target, but do gather their deps'. - for dep in getattr(ctx.rule.attr, "embed", []): - if GoPkgInfo in dep: - pkg_info = dep[GoPkgInfo] - deps_transitive_json_file.append(pkg_info.deps_transitive_json_file) - deps_transitive_export_file.append(pkg_info.deps_transitive_export_file) - - pkg_json_file = None - export_file = None + deps_transitive_compiled_go_files = [] + + for attr in ["deps", "embed"]: + for dep in getattr(ctx.rule.attr, attr, []): + if GoPkgInfo in dep: + pkg_info = dep[GoPkgInfo] + if attr == "deps": + deps_transitive_json_file.append(pkg_info.transitive_json_file) + deps_transitive_export_file.append(pkg_info.transitive_export_file) + deps_transitive_compiled_go_files.append(pkg_info.transitive_compiled_go_files) + elif attr == "embed": + # If deps are embedded, do not gather their json or export_file since they + # are included in the current target, but do gather their deps'. + deps_transitive_json_file.append(pkg_info.deps_transitive_json_file) + deps_transitive_export_file.append(pkg_info.deps_transitive_export_file) + deps_transitive_compiled_go_files.append(pkg_info.deps_transitive_compiled_go_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] - export_file = archive.data.export_file - pkg = 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 - ], - CompiledGoFiles = [ - _file_path(src) - for src in archive.data.srcs - ], - ) - pkg_json_file = ctx.actions.declare_file(archive.data.name + ".pkg.json") - ctx.actions.write(pkg_json_file, content = pkg.to_json()) - # 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 + 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, pkg)) + + # if the rule is a test, we need to get the embedded go_library with the current + # test's sources. For that, consume the dependency via GoArchive.direct so that + # the test source files are there. Then, create the pkg json file directly. Only + # do that for direct dependencies that are not defined as deps, and use the + # importpath to find which. + if ctx.rule.kind == "go_test": + deps_targets = [ + dep[GoArchive].data.importpath + for dep in ctx.rule.attr.deps + if GoArchive in dep + ] + for archive in target[GoArchive].direct: + if archive.data.importpath not in deps_targets: + pkg = _go_archive_to_pkg(archive) + pkg_json_files.append(_make_pkg_json(ctx, archive, pkg)) + compiled_go_files.extend(archive.source.srcs) + export_files.append(archive.data.export_file) + + # 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( - json = pkg_json_file, stdlib_json_file = stdlib_json_file, transitive_json_file = depset( - direct = [pkg_json_file] if pkg_json_file else [], + direct = pkg_json_files, transitive = deps_transitive_json_file, ), deps_transitive_json_file = depset( transitive = deps_transitive_json_file, ), - export_file = export_file, + transitive_compiled_go_files = depset( + direct = compiled_go_files, + transitive = deps_transitive_compiled_go_files, + ), + deps_transitive_compiled_go_files = depset( + transitive = deps_transitive_compiled_go_files, + ), transitive_export_file = depset( - direct = [export_file] if export_file else [], + direct = export_files, transitive = deps_transitive_export_file, ), deps_transitive_export_file = depset( @@ -112,8 +145,9 @@ def _go_pkg_info_aspect_impl(target, ctx): pkg_info, OutputGroupInfo( go_pkg_driver_json_file = pkg_info.transitive_json_file, + go_pkg_driver_srcs = pkg_info.transitive_compiled_go_files, go_pkg_driver_export_file = pkg_info.transitive_export_file, - go_pkg_driver_stdlib_json_file = depset([pkg_info.stdlib_json_file] if pkg_info.stdlib_json_file else []) + go_pkg_driver_stdlib_json_file = depset([pkg_info.stdlib_json_file] if pkg_info.stdlib_json_file else []), ), ] diff --git a/go/tools/gopackagesdriver/bazel.go b/go/tools/gopackagesdriver/bazel.go index 772a86c0a2..881ccc70b3 100644 --- a/go/tools/gopackagesdriver/bazel.go +++ b/go/tools/gopackagesdriver/bazel.go @@ -15,8 +15,11 @@ package main import ( + "bufio" + "bytes" "context" "encoding/json" + "errors" "fmt" "io/ioutil" "net/url" @@ -32,8 +35,8 @@ const ( type Bazel struct { bazelBin string - execRoot string workspaceRoot string + info map[string]string } // Minimal BEP structs to access the build outputs @@ -51,14 +54,26 @@ func NewBazel(ctx context.Context, bazelBin, workspaceRoot string) (*Bazel, erro bazelBin: bazelBin, workspaceRoot: workspaceRoot, } - if execRoot, err := b.run(ctx, "info", "execution_root"); err != nil { - return nil, fmt.Errorf("unable to find execution root: %w", err) - } else { - b.execRoot = strings.TrimSpace(execRoot) + 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) { cmd := exec.CommandContext(ctx, b.bazelBin, append([]string{ command, @@ -66,7 +81,7 @@ func (b *Bazel) run(ctx context.Context, command string, args ...string) (string "--ui_actions_shown=0", }, args...)...) fmt.Fprintln(os.Stderr, "Running:", cmd.Args) - cmd.Dir = b.workspaceRoot + cmd.Dir = b.WorkspaceRoot() cmd.Stderr = os.Stderr output, err := cmd.Output() return string(output), err @@ -88,7 +103,13 @@ func (b *Bazel) Build(ctx context.Context, args ...string) ([]string, error) { "--build_event_json_file_path_conversion=no", }, args...) if _, err := b.run(ctx, "build", args...); err != nil { - return nil, fmt.Errorf("bazel build failed: %w", err) + // 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) @@ -120,10 +141,22 @@ func (b *Bazel) Query(ctx context.Context, args ...string) ([]string, error) { return strings.Split(strings.TrimSpace(output), "\n"), nil } +func (b *Bazel) QueryLabels(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) + } + return strings.Split(strings.TrimSpace(output), "\n"), nil +} + func (b *Bazel) WorkspaceRoot() string { return b.workspaceRoot } func (b *Bazel) ExecutionRoot() string { - return b.execRoot + 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 index 0950a45f72..14f001888e 100644 --- a/go/tools/gopackagesdriver/bazel_json_builder.go +++ b/go/tools/gopackagesdriver/bazel_json_builder.go @@ -17,80 +17,123 @@ package main import ( "context" "fmt" + "path/filepath" "strings" ) type BazelJSONBuilder struct { - bazel *Bazel - query string - tagFilters string - targets []string + bazel *Bazel + requests []string } const ( - OutputGroupDriverJSONFile = "go_pkg_driver_json_file" - OutputGroupStdLibJSONFile = "go_pkg_driver_stdlib_json_file" - OutputGroupExportFile = "go_pkg_driver_export_file" + RulesGoStdlibLabel = "@io_bazel_rules_go//:stdlib" ) -func NewBazelJSONBuilder(bazel *Bazel, query, tagFilters string, targets []string) (*BazelJSONBuilder, error) { +func (b *BazelJSONBuilder) fileQuery(filename string) string { + if filepath.IsAbs(filename) { + fp, _ := filepath.Rel(b.bazel.WorkspaceRoot(), filename) + filename = fp + } + return fmt.Sprintf(`kind("go_library|go_test", same_pkg_direct_rdeps("%s"))`, filename) +} + +func (b *BazelJSONBuilder) packageQuery(importPath string) string { + if strings.HasSuffix(importPath, "/...") { + importPath = fmt.Sprintf(`^%s(/.+)?$`, strings.TrimSuffix(importPath, "/...")) + } + return fmt.Sprintf(`kind("go_library", attr(importpath, "%s", deps(%s)))`, importPath, bazelQueryScope) +} + +func (b *BazelJSONBuilder) queryFromRequests(requests ...string) string { + ret := make([]string, 0, len(requests)) + for _, request := range requests { + result := "" + if request == "." || request == "./..." { + if bazelQueryScope != "" { + result = fmt.Sprintf(`kind("go_library", %s)`, bazelQueryScope) + } else { + result = fmt.Sprintf(RulesGoStdlibLabel) + } + } else if request == "builtin" || request == "std" { + result = fmt.Sprintf(RulesGoStdlibLabel) + } else if strings.HasPrefix(request, "file=") { + f := strings.TrimPrefix(request, "file=") + result = b.fileQuery(f) + } 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, requests ...string) (*BazelJSONBuilder, error) { return &BazelJSONBuilder{ - bazel: bazel, - query: query, - tagFilters: tagFilters, - targets: targets, + bazel: bazel, + requests: requests, }, nil } func (b *BazelJSONBuilder) outputGroupsForMode(mode LoadMode) string { - og := OutputGroupDriverJSONFile + "," + OutputGroupStdLibJSONFile - if mode&NeedExportsFile != 0 || true { // override for now - og += "," + OutputGroupExportFile + 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) Build(ctx context.Context, mode LoadMode) ([]string, error) { - buildsArgs := []string{ - "--aspects=@io_bazel_rules_go//go/tools/gopackagesdriver:aspect.bzl%go_pkg_info_aspect", - "--output_groups=" + b.outputGroupsForMode(mode), - "--keep_going", // Build all possible packages +func (b *BazelJSONBuilder) query(ctx context.Context, query string) ([]string, error) { + queryArgs := concatStringsArrays(bazelFlags, 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 +} - if b.tagFilters != "" { - buildsArgs = append(buildsArgs, "--build_tag_filters="+b.tagFilters) +func (b *BazelJSONBuilder) Build(ctx context.Context, mode LoadMode) ([]string, error) { + labels, err := b.query(ctx, b.queryFromRequests(b.requests...)) + if err != nil { + return nil, fmt.Errorf("query failed: %w", err) } - if b.query != "" { - queryTargets, err := b.bazel.Query( - ctx, - "--order_output=no", - "--output=label", - "--experimental_graphless_query", - "--nodep_deps", - "--noimplicit_deps", - "--notool_deps", - b.query, - ) - if err != nil { - return nil, fmt.Errorf("unable to query %v: %w", b.query, err) - } - buildsArgs = append(buildsArgs, queryTargets...) + if len(labels) == 0 { + return nil, fmt.Errorf("found no labels matching the requests") } - buildsArgs = append(buildsArgs, b.targets...) - - files, err := b.bazel.Build(ctx, buildsArgs...) + buildArgs := concatStringsArrays([]string{ + "--experimental_convenience_symlinks=ignore", + "--ui_event_filters=-info,-stderr", + "--noshow_progress", + "--aspects=" + rulesGoRepositoryName + "//go/tools/gopackagesdriver:aspect.bzl%go_pkg_info_aspect", + "--output_groups=" + b.outputGroupsForMode(mode), + "--keep_going", // Build all possible packages + }, bazelFlags, bazelBuildFlags, labels) + files, err := b.bazel.Build(ctx, buildArgs...) if err != nil { - return nil, fmt.Errorf("unable to bazel build %v: %w", buildsArgs, err) + return nil, fmt.Errorf("unable to bazel build %v: %w", buildArgs, err) } ret := []string{} for _, f := range files { - if !strings.HasSuffix(f, ".pkg.json") { - continue + if strings.HasSuffix(f, ".pkg.json") { + ret = append(ret, f) } - ret = append(ret, f) } return ret, nil @@ -100,6 +143,7 @@ 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 0000000000..8074746c9f --- /dev/null +++ b/go/tools/gopackagesdriver/build_context.go @@ -0,0 +1,38 @@ +package main + +import ( + "go/build" + "os" + "path/filepath" + "strings" +) + +var buildContext = makeBuildContext() + +func makeBuildContext() *build.Context { + bctx := &build.Context{ + GOOS: getenvDefault("GOOS", build.Default.GOOS), + GOARCH: getenvDefault("GOARCH", build.Default.GOARCH), + GOROOT: getenvDefault("GOROOT", build.Default.GOROOT), + GOPATH: getenvDefault("GOPATH", build.Default.GOPATH), + BuildTags: strings.Split(getenvDefault("GOTAGS", ""), ","), + ReleaseTags: build.Default.ReleaseTags[:], + } + if v, ok := os.LookupEnv("CGO_ENABLED"); ok { + bctx.CgoEnabled = v == "1" + } else { + bctx.CgoEnabled = build.Default.CgoEnabled + } + return bctx +} + +func filterSourceFilesForTags(files []string) []string { + ret := make([]string, 0, len(files)) + for _, f := range files { + dir, filename := filepath.Split(f) + if match, _ := buildContext.MatchFile(dir, filename); match { + ret = append(ret, f) + } + } + return ret +} diff --git a/go/tools/gopackagesdriver/driver_request.go b/go/tools/gopackagesdriver/driver_request.go index 8fe2f63e22..1b82ed14e7 100644 --- a/go/tools/gopackagesdriver/driver_request.go +++ b/go/tools/gopackagesdriver/driver_request.go @@ -65,7 +65,7 @@ const ( ) // From https://github.com/golang/tools/blob/v0.1.0/go/packages/external.go#L32 -// Most fields are disabled since there are no needs for them +// 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. diff --git a/go/tools/gopackagesdriver/flatpackage.go b/go/tools/gopackagesdriver/flatpackage.go index b0fbe0ff36..885acfd42e 100644 --- a/go/tools/gopackagesdriver/flatpackage.go +++ b/go/tools/gopackagesdriver/flatpackage.go @@ -65,8 +65,10 @@ type FlatPackage struct { Standard bool `json:",omitempty"` } -type PackageFunc func(pkg *FlatPackage) -type PathResolverFunc func(path string) string +type ( + PackageFunc func(pkg *FlatPackage) + PathResolverFunc func(path string) string +) func resolvePathsInPlace(prf PathResolverFunc, paths []string) { for i, path := range paths { @@ -100,6 +102,13 @@ func (fp *FlatPackage) ResolvePaths(prf PathResolverFunc) error { 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 } diff --git a/go/tools/gopackagesdriver/main.go b/go/tools/gopackagesdriver/main.go index a67b3ad54e..17ee60a470 100644 --- a/go/tools/gopackagesdriver/main.go +++ b/go/tools/gopackagesdriver/main.go @@ -20,11 +20,15 @@ import ( "fmt" "go/types" "os" - "os/signal" "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. @@ -45,77 +49,67 @@ type driverResponse struct { } var ( - bazelBin = getenvDefault("GOPACKAGESDRIVER_BAZEL", "bazel") - workspaceRoot = os.Getenv("BUILD_WORKSPACE_DIRECTORY") - targets = strings.Fields(os.Getenv("GOPACKAGESDRIVER_BAZEL_TARGETS")) - targetsQueryStr = os.Getenv("GOPACKAGESDRIVER_BAZEL_QUERY") - targetsTagFilters = os.Getenv("GOPACKAGESDRIVER_BAZEL_TAG_FILTERS") -) - -func getenvDefault(key, defaultValue string) string { - if v, ok := os.LookupEnv(key); ok { - return v + // It seems https://github.com/bazelbuild/bazel/issues/3115 isn't fixed when specifying + // the aspect from the command line. Use this trick in the mean time. + rulesGoRepositoryName = getenvDefault("GOPACKAGESDRIVER_RULES_GO_REPOSITORY_NAME", "@io_bazel_rules_go") + bazelBin = getenvDefault("GOPACKAGESDRIVER_BAZEL", "bazel") + bazelFlags = 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") + emptyResponse = &driverResponse{ + NotHandled: false, + Sizes: types.SizesFor("gc", "amd64").(*types.StdSizes), + Roots: []string{}, + Packages: []*FlatPackage{}, } - return defaultValue -} - -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 run() error { +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 fmt.Errorf("unable to read request: %w", err) + return emptyResponse, fmt.Errorf("unable to read request: %w", err) } bazel, err := NewBazel(ctx, bazelBin, workspaceRoot) if err != nil { - return fmt.Errorf("unable to create bazel instance: %w", err) + return emptyResponse, fmt.Errorf("unable to create bazel instance: %w", err) } - bazelJsonBuilder, err := NewBazelJSONBuilder(bazel, targetsQueryStr, targetsTagFilters, targets) + bazelJsonBuilder, err := NewBazelJSONBuilder(bazel, queries...) if err != nil { - return fmt.Errorf("unable to build JSON files: %w", err) + return emptyResponse, fmt.Errorf("unable to build JSON files: %w", err) } jsonFiles, err := bazelJsonBuilder.Build(ctx, request.Mode) if err != nil { - return fmt.Errorf("unable to build JSON files: %w", err) + return emptyResponse, fmt.Errorf("unable to build JSON files: %w", err) } driver, err := NewJSONPackagesDriver(jsonFiles, bazelJsonBuilder.PathResolver()) if err != nil { - return fmt.Errorf("unable to load JSON files: %w", err) + return emptyResponse, fmt.Errorf("unable to load JSON files: %w", err) } - response := driver.Match(os.Args[1:]...) - // return nil - - if err := json.NewEncoder(os.Stdout).Encode(response); err != nil { - return fmt.Errorf("unable to encode response: %w", err) - } - - return nil + return driver.Match(queries...), nil } func main() { - if err := run(); err != nil { + 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) - os.Exit(1) + // 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 index cd979d84d5..8c783abbf6 100644 --- a/go/tools/gopackagesdriver/packageregistry.go +++ b/go/tools/gopackagesdriver/packageregistry.go @@ -56,6 +56,10 @@ func (pr *PackageRegistry) Remove(pkgs ...*FlatPackage) *PackageRegistry { func (pr *PackageRegistry) ResolvePaths(prf PathResolverFunc) error { for _, pkg := range pr.packagesByImportPath { pkg.ResolvePaths(prf) + pkg.FilterFilesForBuildTags() + for _, f := range pkg.CompiledGoFiles { + pr.packagesByFile[f] = pkg + } for _, f := range pkg.CompiledGoFiles { pr.packagesByFile[f] = pkg } @@ -84,16 +88,32 @@ func (pr *PackageRegistry) walk(acc map[string]*FlatPackage, root string) { func (pr *PackageRegistry) Match(patterns ...string) ([]string, []*FlatPackage) { roots := map[string]struct{}{} - wildcard := false for _, pattern := range patterns { - if strings.HasPrefix(pattern, "file=") { - f := strings.TrimPrefix(pattern, "file=") + if pattern == "." || pattern == "./..." { + for _, pkg := range pr.packagesByImportPath { + if strings.HasPrefix(pkg.ID, "//") { + roots[pkg.ID] = struct{}{} + } + } + } else if strings.HasSuffix(pattern, "/...") { + pkgPrefix := strings.TrimSuffix(pattern, "/...") + for _, pkg := range pr.packagesByImportPath { + if pkgPrefix == pkg.PkgPath || strings.HasPrefix(pkg.PkgPath, pkgPrefix+"/") { + roots[pkg.ID] = struct{}{} + } + } + } else if pattern == "builtin" || pattern == "std" { + for _, pkg := range pr.packagesByImportPath { + if pkg.Standard { + roots[pkg.ID] = struct{}{} + } + } + } else if strings.HasPrefix(pattern, "file=") { + f := ensureAbsolutePathFromWorkspace(strings.TrimPrefix(pattern, "file=")) if pkg, ok := pr.packagesByFile[f]; ok { roots[pkg.ID] = struct{}{} } - } else if pattern == "." || pattern == "./..." { - wildcard = true } else { if pkg, ok := pr.packagesByImportPath[pattern]; ok { roots[pkg.ID] = struct{}{} @@ -101,19 +121,6 @@ func (pr *PackageRegistry) Match(patterns ...string) ([]string, []*FlatPackage) } } - if wildcard { - retPkgs := make([]*FlatPackage, 0, len(pr.packagesByImportPath)) - retRoots := make([]string, 0, len(pr.packagesByImportPath)) - for _, pkg := range pr.packagesByImportPath { - if strings.HasPrefix(pkg.ID, "//") { - retRoots = append(retRoots, pkg.ID) - roots[pkg.ID] = struct{}{} - } - retPkgs = append(retPkgs, pkg) - } - return retRoots, retPkgs - } - walkedPackages := map[string]*FlatPackage{} retRoots := make([]string, 0, len(roots)) for rootPkg := range roots { diff --git a/go/tools/gopackagesdriver/utils.go b/go/tools/gopackagesdriver/utils.go new file mode 100644 index 0000000000..4418a41e6c --- /dev/null +++ b/go/tools/gopackagesdriver/utils.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 ( + "context" + "os" + "os/signal" + "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 +} diff --git a/tools/gopackagesdriver.sh b/tools/gopackagesdriver.sh new file mode 100755 index 0000000000..b29cc410ef --- /dev/null +++ b/tools/gopackagesdriver.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +export GOPACKAGESDRIVER_RULES_GO_REPOSITORY_NAME= +exec bazel run --tool_tag=gopackagesdriver -- //go/tools/gopackagesdriver "${@}"