diff --git a/go/BUILD.bazel b/go/BUILD.bazel index f582021b69..9c2c994992 100644 --- a/go/BUILD.bazel +++ b/go/BUILD.bazel @@ -51,6 +51,7 @@ bzl_library( "//go/private:context", "//go/private:go_toolchain", "//go/private:providers", + "//go/private/aspects:all_rules", "//go/private/rules:library", "//go/private/rules:nogo", "//go/private/rules:sdk", diff --git a/go/private/actions/archive.bzl b/go/private/actions/archive.bzl index 8b17c1926e..4e3446e5dc 100644 --- a/go/private/actions/archive.bzl +++ b/go/private/actions/archive.bzl @@ -167,6 +167,8 @@ def emit_archive(go, source = None, _recompile_suffix = "", recompile_internal_d is_external_pkg = is_external_pkg, ) + # Buildinfo metadata is collected via aspect and stored separately + data = GoArchiveData( # TODO(#2578): reconsider the provider API. There's a lot of redundant # information here. Some fields are tuples instead of lists or dicts diff --git a/go/private/actions/binary.bzl b/go/private/actions/binary.bzl index 4af8109daf..97d8a9d8b8 100644 --- a/go/private/actions/binary.bzl +++ b/go/private/actions/binary.bzl @@ -34,6 +34,8 @@ def emit_binary( gc_linkopts = [], version_file = None, info_file = None, + buildinfo_metadata = None, + target_label = None, executable = None): """See go/toolchains.rst#binary for full documentation.""" @@ -61,6 +63,8 @@ def emit_binary( gc_linkopts = gc_linkopts, version_file = version_file, info_file = info_file, + buildinfo_metadata = buildinfo_metadata, + target_label = target_label, ) cgo_dynamic_deps = [ d diff --git a/go/private/actions/link.bzl b/go/private/actions/link.bzl index 7d8ff5f0f6..00517e673e 100644 --- a/go/private/actions/link.bzl +++ b/go/private/actions/link.bzl @@ -44,7 +44,9 @@ def emit_link( executable = None, gc_linkopts = [], version_file = None, - info_file = None): + info_file = None, + buildinfo_metadata = None, + target_label = None): """See go/toolchains.rst#link for full documentation.""" if archive == None: @@ -52,6 +54,60 @@ def emit_link( if executable == None: fail("executable is a required parameter") + # Generate buildinfo dependency file for Go 1.18+ buildInfo support + buildinfo_file = None + if buildinfo_metadata: + buildinfo_file = go.declare_file(go, path = executable.basename + ".buildinfo.txt") + + # Materialize depsets at link time + importpaths = buildinfo_metadata.importpaths.to_list() + metadata_targets = buildinfo_metadata.metadata_providers.to_list() + + # Build version map from metadata providers + # Sort targets by label for deterministic version resolution + # This ensures reproducible builds and consistent action cache keys + sorted_targets = sorted( + metadata_targets, + key = lambda t: str(t.label) if hasattr(t, "label") else str(t), + ) + + version_map = {} + for target in sorted_targets: + # Extract version info from PackageInfo provider + if hasattr(target, "package_info"): + # Handle both single object and list of package_info + package_infos = target.package_info if type(target.package_info) == type([]) else [target.package_info] + for info in package_infos: + if hasattr(info, "module") and hasattr(info, "version"): + module = info.module + version = info.version + + # Use first version (by sorted label order) if duplicates exist + # This makes conflicts deterministic and debuggable + if module not in version_map: + version_map[module] = version + + # Build buildinfo content + content_lines = [] + + # Add main package path + if archive.data.importpath: + content_lines.append("path\t{}".format(archive.data.importpath)) + + # Add dependencies with versions + # Sort importpaths for deterministic output, which is required for: + # 1. Bazel action caching - identical inputs must produce identical outputs + # 2. Reproducible builds across different machines + # 3. Easier debugging and testing with predictable output order + for importpath in sorted(importpaths): + version = version_map.get(importpath, "(devel)") + content_lines.append("dep\t{}\t{}".format(importpath, version)) + + go.actions.write( + output = buildinfo_file, + content = "\n".join(content_lines) + "\n" if content_lines else "", + ) + # Exclude -lstdc++ from link options. We don't want to link against it # unless we actually have some C++ code. _cgo_codegen will include it # in archives via CGO_LDFLAGS if it's needed. @@ -167,6 +223,13 @@ def emit_link( builder_args.add("-o", executable) builder_args.add("-main", archive.data.file) builder_args.add("-p", archive.data.importmap) + + # Pass buildinfo file to builder if available + if buildinfo_file: + builder_args.add("-buildinfo", buildinfo_file) + if target_label: + builder_args.add("-bazeltarget", target_label) + tool_args.add_all(gc_linkopts) tool_args.add_all(go.toolchain.flags.link) @@ -177,6 +240,8 @@ def emit_link( tool_args.add_joined("-extldflags", extldflags, join_with = " ") inputs_direct = stamp_inputs + [go.sdk.package_list] + if buildinfo_file: + inputs_direct.append(buildinfo_file) if go.coverage_enabled and go.coverdata: inputs_direct.append(go.coverdata.data.file) inputs_transitive = [ diff --git a/go/private/aspects/BUILD.bazel b/go/private/aspects/BUILD.bazel new file mode 100644 index 0000000000..648cac55e0 --- /dev/null +++ b/go/private/aspects/BUILD.bazel @@ -0,0 +1,12 @@ +filegroup( + name = "all_rules", + srcs = glob(["**/*.bzl"]), + visibility = ["//visibility:public"], +) + +filegroup( + name = "all_files", + testonly = True, + srcs = glob(["**"]), + visibility = ["//visibility:public"], +) diff --git a/go/private/aspects/buildinfo_aspect.bzl b/go/private/aspects/buildinfo_aspect.bzl new file mode 100644 index 0000000000..d9254c6c6d --- /dev/null +++ b/go/private/aspects/buildinfo_aspect.bzl @@ -0,0 +1,100 @@ +"""Aspect to collect package_info metadata for buildInfo. + +This aspect traverses Go binary dependencies and collects version information +from Gazelle-generated package_info targets referenced via the package_metadata +common attribute (inherited from REPO.bazel default_package_metadata). + +Implementation based on bazel-contrib/supply-chain gather_metadata pattern. +Currently doesn't use the supply chain tools dep for this as it is not yet +stable and we still need to support WORKSPACE which the supply-chain tools +doesn't have support for. +""" + +load( + "//go/private:providers.bzl", + "GoArchive", +) + +visibility(["//go/private/..."]) + +BuildInfoMetadata = provider( + doc = "INTERNAL: Provides dependency version metadata for buildInfo. Do not depend on this provider.", + fields = { + "importpaths": "Depset of importpath strings from Go dependencies", + "metadata_providers": "Depset of PackageInfo providers with version data", + }, +) + +def _buildinfo_aspect_impl(target, ctx): + """Collects package_info metadata from dependencies. + + This aspect collects version information from package_metadata attributes + (set via REPO.bazel default_package_metadata in go_repository) and tracks + importpaths from Go dependencies. The actual version matching is deferred + to execution time to avoid quadratic memory usage. + + Args: + target: The target being visited + ctx: The aspect context + + Returns: + List containing BuildInfoMetadata provider + """ + direct_importpaths = [] + direct_metadata = [] + transitive_importpaths = [] + transitive_metadata = [] + + # Collect importpath from this target if it's a Go target + if GoArchive in target: + importpath = target[GoArchive].data.importpath + if importpath: + direct_importpaths.append(importpath) + + # Check package_metadata common attribute (Bazel 6+) + # This is set via REPO.bazel default_package_metadata in go_repository + if hasattr(ctx.rule.attr, "package_metadata"): + attr_value = ctx.rule.attr.package_metadata + if attr_value: + package_metadata = attr_value if type(attr_value) == type([]) else [attr_value] + + # Store the metadata targets directly for later processing + direct_metadata.extend(package_metadata) + + # Collect transitive metadata from Go dependencies only + # Only traverse deps and embed to avoid non-Go dependencies + for attr_name in ["deps", "embed"]: + if not hasattr(ctx.rule.attr, attr_name): + continue + + attr_value = getattr(ctx.rule.attr, attr_name) + if not attr_value: + continue + + deps = attr_value if type(attr_value) == type([]) else [attr_value] + for dep in deps: + # Collect transitive BuildInfoMetadata + if BuildInfoMetadata in dep: + transitive_importpaths.append(dep[BuildInfoMetadata].importpaths) + transitive_metadata.append(dep[BuildInfoMetadata].metadata_providers) + + # Build depsets (empty depsets are efficient, no need for early return) + return [BuildInfoMetadata( + importpaths = depset( + direct = direct_importpaths, + transitive = transitive_importpaths, + ), + metadata_providers = depset( + direct = direct_metadata, + transitive = transitive_metadata, + ), + )] + +buildinfo_aspect = aspect( + doc = "Collects package_info metadata for Go buildInfo", + implementation = _buildinfo_aspect_impl, + attr_aspects = ["deps", "embed"], # Only traverse Go dependencies + provides = [BuildInfoMetadata], + # Apply to generated targets including package_info + apply_to_generating_rules = True, +) diff --git a/go/private/rules/BUILD.bazel b/go/private/rules/BUILD.bazel index 25d8e37f72..6ed3fdd99a 100644 --- a/go/private/rules/BUILD.bazel +++ b/go/private/rules/BUILD.bazel @@ -45,6 +45,7 @@ bzl_library( "//go/private:mode", "//go/private:providers", "//go/private:rpath", + "//go/private/aspects:all_rules", "//go/private/rules:transition", ], ) diff --git a/go/private/rules/binary.bzl b/go/private/rules/binary.bzl index 730bcda345..76ac1fe01b 100644 --- a/go/private/rules/binary.bzl +++ b/go/private/rules/binary.bzl @@ -47,6 +47,11 @@ load( "GoInfo", "GoSDK", ) +load( + "//go/private/aspects:buildinfo_aspect.bzl", + "BuildInfoMetadata", + "buildinfo_aspect", +) load( "//go/private/rules:transition.bzl", "go_transition", @@ -155,6 +160,28 @@ def _go_binary_impl(ctx): importable = False, is_main = is_main, ) + + # Collect version metadata from aspect for buildInfo + # Merge ALL metadata from all deps and embed targets to ensure complete coverage + all_importpaths = [] + all_metadata = [] + for attr_name in ["embed", "deps"]: + if not hasattr(ctx.attr, attr_name): + continue + for target in getattr(ctx.attr, attr_name): + if BuildInfoMetadata in target: + all_importpaths.append(target[BuildInfoMetadata].importpaths) + all_metadata.append(target[BuildInfoMetadata].metadata_providers) + + buildinfo_metadata = None + if all_importpaths: + buildinfo_metadata = BuildInfoMetadata( + importpaths = depset(transitive = all_importpaths), + metadata_providers = depset(transitive = all_metadata), + ) + + # Get Bazel target label for buildInfo + target_label = str(ctx.label) name = ctx.attr.basename if not name: name = ctx.label.name @@ -172,6 +199,8 @@ def _go_binary_impl(ctx): version_file = ctx.version_file, info_file = ctx.info_file, executable = executable, + buildinfo_metadata = buildinfo_metadata, + target_label = target_label, ) validation_output = archive.data._validation_output nogo_diagnostics = archive.data._nogo_diagnostics @@ -279,14 +308,14 @@ def _go_binary_kwargs(go_cc_aspects = []): ), "deps": attr.label_list( providers = [GoInfo], - aspects = go_cc_aspects, + aspects = go_cc_aspects + [buildinfo_aspect], doc = """List of Go libraries this package imports directly. These may be `go_library` rules or compatible rules with the [GoInfo] provider. """, ), "embed": attr.label_list( providers = [GoInfo], - aspects = go_cc_aspects, + aspects = go_cc_aspects + [buildinfo_aspect], doc = """List of Go libraries whose sources should be compiled together with this binary's sources. Labels listed here must name `go_library`, `go_proto_library`, or other compatible targets with the [GoInfo] provider. diff --git a/go/tools/builders/BUILD.bazel b/go/tools/builders/BUILD.bazel index 42b26a2446..4134365d5a 100644 --- a/go/tools/builders/BUILD.bazel +++ b/go/tools/builders/BUILD.bazel @@ -86,6 +86,7 @@ filegroup( "ar.go", "asm.go", "builder.go", + "buildinfo.go", "cc.go", "cgo2.go", "compilepkg.go", @@ -101,6 +102,7 @@ filegroup( "generate_test_main.go", "importcfg.go", "link.go", + "modinfo.go", "nogo.go", "nogo_validation.go", "read.go", diff --git a/go/tools/builders/buildinfo.go b/go/tools/builders/buildinfo.go new file mode 100644 index 0000000000..58ec8be8c5 --- /dev/null +++ b/go/tools/builders/buildinfo.go @@ -0,0 +1,127 @@ +// ORIGINAL: https://github.com/golang/go/blob/f4e37b8afc01253567fddbdd68ec35632df86b62/src/runtime/debug/mod.go +// +// Like the above, but without ReadBuildInfo and ParseBuildInfo, as well as a different package name. + +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "strconv" + "strings" +) + +// BuildInfo represents the build information read from a Go binary. +type BuildInfo struct { + // GoVersion is the version of the Go toolchain that built the binary + // (for example, "go1.19.2"). + GoVersion string + + // Path is the package path of the main package for the binary + // (for example, "golang.org/x/tools/cmd/stringer"). + Path string + + // Main describes the module that contains the main package for the binary. + Main Module + + // Deps describes all the dependency modules, both direct and indirect, + // that contributed packages to the build of this binary. + Deps []*Module + + // Settings describes the build settings used to build the binary. + Settings []BuildSetting +} + +// A Module describes a single module included in a build. +type Module struct { + Path string // module path + Version string // module version + Sum string // checksum + Replace *Module // replaced by this module +} + +// A BuildSetting is a key-value pair describing one setting that influenced a build. +// +// Defined keys include: +// +// - -buildmode: the buildmode flag used (typically "exe") +// - -compiler: the compiler toolchain flag used (typically "gc") +// - CGO_ENABLED: the effective CGO_ENABLED environment variable +// - CGO_CFLAGS: the effective CGO_CFLAGS environment variable +// - CGO_CPPFLAGS: the effective CGO_CPPFLAGS environment variable +// - CGO_CXXFLAGS: the effective CGO_CXXFLAGS environment variable +// - CGO_LDFLAGS: the effective CGO_LDFLAGS environment variable +// - DefaultGODEBUG: the effective GODEBUG settings +// - GOARCH: the architecture target +// - GOAMD64/GOARM/GO386/etc: the architecture feature level for GOARCH +// - GOOS: the operating system target +// - GOFIPS140: the frozen FIPS 140-3 module version, if any +// - vcs: the version control system for the source tree where the build ran +// - vcs.revision: the revision identifier for the current commit or checkout +// - vcs.time: the modification time associated with vcs.revision, in RFC3339 format +// - vcs.modified: true or false indicating whether the source tree had local modifications +type BuildSetting struct { + // Key and Value describe the build setting. + // Key must not contain an equals sign, space, tab, or newline. + // Value must not contain newlines ('\n'). + Key, Value string +} + +// quoteKey reports whether key is required to be quoted. +func quoteKey(key string) bool { + return len(key) == 0 || strings.ContainsAny(key, "= \t\r\n\"`") +} + +// quoteValue reports whether value is required to be quoted. +func quoteValue(value string) bool { + return strings.ContainsAny(value, " \t\r\n\"`") +} + +// String returns a string representation of a [BuildInfo]. +func (bi *BuildInfo) String() string { + buf := new(strings.Builder) + if bi.GoVersion != "" { + fmt.Fprintf(buf, "go\t%s\n", bi.GoVersion) + } + if bi.Path != "" { + fmt.Fprintf(buf, "path\t%s\n", bi.Path) + } + var formatMod func(string, Module) + formatMod = func(word string, m Module) { + buf.WriteString(word) + buf.WriteByte('\t') + buf.WriteString(m.Path) + buf.WriteByte('\t') + buf.WriteString(m.Version) + if m.Replace == nil { + buf.WriteByte('\t') + buf.WriteString(m.Sum) + } else { + buf.WriteByte('\n') + formatMod("=>", *m.Replace) + } + buf.WriteByte('\n') + } + if bi.Main != (Module{}) { + formatMod("mod", bi.Main) + } + for _, dep := range bi.Deps { + formatMod("dep", *dep) + } + for _, s := range bi.Settings { + key := s.Key + if quoteKey(key) { + key = strconv.Quote(key) + } + value := s.Value + if quoteValue(value) { + value = strconv.Quote(value) + } + fmt.Fprintf(buf, "build\t%s=%s\n", key, value) + } + + return buf.String() +} diff --git a/go/tools/builders/importcfg.go b/go/tools/builders/importcfg.go index c25763a02c..9dc499ed75 100644 --- a/go/tools/builders/importcfg.go +++ b/go/tools/builders/importcfg.go @@ -20,9 +20,9 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "path/filepath" + "runtime" "sort" "strings" ) @@ -39,11 +39,11 @@ type archive struct { // for standard library packages. func checkImports(files []fileInfo, archives []archive, stdPackageListPath string, importPath string, recompileInternalDeps []string) (map[string]*archive, error) { // Read the standard package list. - packagesTxt, err := ioutil.ReadFile(stdPackageListPath) + packagesTxt, err := os.ReadFile(stdPackageListPath) if err != nil { - return nil, err + return nil, fmt.Errorf("reading standard package list: %w", err) } - stdPkgs := make(map[string]bool) + stdPkgs := make(map[string]struct{}) for len(packagesTxt) > 0 { n := bytes.IndexByte(packagesTxt, '\n') var line string @@ -58,7 +58,7 @@ func checkImports(files []fileInfo, archives []archive, stdPackageListPath strin if line == "" { continue } - stdPkgs[line] = true + stdPkgs[line] = struct{}{} } // Index the archives. @@ -90,7 +90,7 @@ func checkImports(files []fileInfo, archives []archive, stdPackageListPath strin if _, ok := recompileInternalDepMap[path]; ok { return nil, fmt.Errorf("dependency cycle detected between %q and %q in file %q", importPath, path, f.filename) } - if stdPkgs[path] { + if _, ok := stdPkgs[path]; ok { imports[path] = nil } else if arc := importToArchive[path]; arc != nil { imports[path] = arc @@ -137,24 +137,46 @@ func buildImportcfgFileForCompile(imports map[string]*archive, installSuffix, di } } - f, err := ioutil.TempFile(dir, "importcfg") + f, err := os.CreateTemp(dir, "importcfg") if err != nil { - return "", err + return "", fmt.Errorf("creating importcfg temp file: %w", err) } filename := f.Name() if _, err := io.Copy(f, buf); err != nil { f.Close() os.Remove(filename) - return "", err + return "", fmt.Errorf("writing importcfg: %w", err) } if err := f.Close(); err != nil { os.Remove(filename) - return "", err + return "", fmt.Errorf("closing importcfg: %w", err) } return filename, nil } -func buildImportcfgFileForLink(archives []archive, stdPackageListPath, installSuffix, dir string) (string, error) { +// linkConfig contains parameters for generating buildinfo +type linkConfig struct { + path string + buildMode string + compiler string + cgoEnabled bool + goarch string + goos string + pgoProfilePath string + buildinfoFile string + deps []*Module + // Architecture feature level (e.g., key="GOAMD64", value="v3") + goarchFeatureKey string + goarchFeatureValue string + // CGO flags + cgoCflags string + cgoCxxflags string + cgoLdflags string + // Bazel metadata + bazelTarget string +} + +func buildImportcfgFileForLink(archives []archive, stdPackageListPath, installSuffix, dir string, cfg linkConfig) (string, error) { buf := &bytes.Buffer{} goroot, ok := os.LookupEnv("GOROOT") if !ok { @@ -163,7 +185,7 @@ func buildImportcfgFileForLink(archives []archive, stdPackageListPath, installSu prefix := abs(filepath.Join(goroot, "pkg", installSuffix)) stdPackageListFile, err := os.Open(stdPackageListPath) if err != nil { - return "", err + return "", fmt.Errorf("opening standard package list %s: %w", stdPackageListPath, err) } defer stdPackageListFile.Close() scanner := bufio.NewScanner(stdPackageListFile) @@ -175,7 +197,7 @@ func buildImportcfgFileForLink(archives []archive, stdPackageListPath, installSu fmt.Fprintf(buf, "packagefile %s=%s.a\n", line, filepath.Join(prefix, filepath.FromSlash(line))) } if err := scanner.Err(); err != nil { - return "", err + return "", fmt.Errorf("scanning standard package list %s: %w", stdPackageListPath, err) } depsSeen := map[string]string{} for _, arc := range archives { @@ -196,19 +218,63 @@ package with this path is linked.`, depsSeen[arc.packagePath] = arc.importPath fmt.Fprintf(buf, "packagefile %s=%s\n", arc.packagePath, arc.file) } - f, err := ioutil.TempFile(dir, "importcfg") + + // Generate buildinfo if a buildinfo file was provided + // This ensures metadata is embedded even when there are no external dependencies + if cfg.buildinfoFile != "" { + buildInfo := BuildInfo{ + GoVersion: runtime.Version(), + Path: cfg.path, + Main: Module{}, + Deps: cfg.deps, + Settings: []BuildSetting{}, + } + + buildSettingAppend := func(key, value string) { + if value != "" { + buildInfo.Settings = append(buildInfo.Settings, BuildSetting{Key: key, Value: value}) + } + } + + buildSettingAppend("-compiler", cfg.compiler) + buildSettingAppend("-buildmode", cfg.buildMode) + if cfg.pgoProfilePath != "" { + buildSettingAppend("-pgo", cfg.pgoProfilePath) + } + if cfg.cgoEnabled { + buildSettingAppend("CGO_ENABLED", "1") + // Add CGO flags when CGO is enabled + buildSettingAppend("CGO_CFLAGS", cfg.cgoCflags) + buildSettingAppend("CGO_CXXFLAGS", cfg.cgoCxxflags) + buildSettingAppend("CGO_LDFLAGS", cfg.cgoLdflags) + } else { + buildSettingAppend("CGO_ENABLED", "0") + } + buildSettingAppend("GOOS", cfg.goos) + buildSettingAppend("GOARCH", cfg.goarch) + // Add architecture feature level if present + if cfg.goarchFeatureKey != "" && cfg.goarchFeatureValue != "" { + buildSettingAppend(cfg.goarchFeatureKey, cfg.goarchFeatureValue) + } + // Add Bazel metadata + buildSettingAppend("bazel.target", cfg.bazelTarget) + + fmt.Fprintf(buf, "modinfo %q\n", ModInfoData(buildInfo.String())) + } + + f, err := os.CreateTemp(dir, "importcfg") if err != nil { - return "", err + return "", fmt.Errorf("creating importcfg temp file: %w", err) } filename := f.Name() if _, err := io.Copy(f, buf); err != nil { f.Close() os.Remove(filename) - return "", err + return "", fmt.Errorf("writing importcfg: %w", err) } if err := f.Close(); err != nil { os.Remove(filename) - return "", err + return "", fmt.Errorf("closing importcfg: %w", err) } return filename, nil } diff --git a/go/tools/builders/link.go b/go/tools/builders/link.go index 11dc0abfe1..1d9db60712 100644 --- a/go/tools/builders/link.go +++ b/go/tools/builders/link.go @@ -21,7 +21,6 @@ import ( "bytes" "flag" "fmt" - "io/ioutil" "os" "path/filepath" "regexp" @@ -30,6 +29,83 @@ import ( "strings" ) +// getArchFeature returns the environment variable key and value for the +// architecture-specific feature level (e.g., GOAMD64=v3, GOARM=7). +// Returns empty strings if no feature level is set for the architecture. +func getArchFeature(goarch string) (key, value string) { + switch goarch { + case "amd64": + if v := os.Getenv("GOAMD64"); v != "" { + return "GOAMD64", v + } + case "arm": + if v := os.Getenv("GOARM"); v != "" { + return "GOARM", v + } + case "386": + if v := os.Getenv("GO386"); v != "" { + return "GO386", v + } + case "mips", "mipsle": + if v := os.Getenv("GOMIPS"); v != "" { + return "GOMIPS", v + } + case "mips64", "mips64le": + if v := os.Getenv("GOMIPS64"); v != "" { + return "GOMIPS64", v + } + case "ppc64", "ppc64le": + if v := os.Getenv("GOPPC64"); v != "" { + return "GOPPC64", v + } + case "riscv64": + if v := os.Getenv("GORISCV64"); v != "" { + return "GORISCV64", v + } + case "wasm": + if v := os.Getenv("GOWASM"); v != "" { + return "GOWASM", v + } + } + return "", "" +} + +// findBestModuleMatch finds the longest matching module path prefix +// for a given importpath. This implements longest-prefix matching for subpackages. +// For example, given importpath "example.com/foo/bar" and modules +// ["example.com/foo", "example.com"], it returns "example.com/foo". +// Returns empty string if no match is found. +func findBestModuleMatch(importpath string, moduleRoots []string) string { + bestMatch := "" + for _, modulePath := range moduleRoots { + if strings.HasPrefix(importpath, modulePath+"/") { + if len(modulePath) > len(bestMatch) { + bestMatch = modulePath + } + } + } + return bestMatch +} + +// parseXdef parses a linker -X flag in the format "package.name=value" +// and returns the package, variable name, and value. +// If pkg matches mainPackagePath, it is rewritten to "main". +func parseXdef(xdef string, mainPackagePath string) (pkg, name, value string, err error) { + eq := strings.IndexByte(xdef, '=') + if eq < 0 { + return "", "", "", fmt.Errorf("-X flag does not contain '=': %s", xdef) + } + dot := strings.LastIndexByte(xdef[:eq], '.') + if dot < 0 { + return "", "", "", fmt.Errorf("-X flag does not contain '.': %s", xdef) + } + pkg, name, value = xdef[:dot], xdef[dot+1:eq], xdef[eq+1:] + if pkg == mainPackagePath { + pkg = "main" + } + return pkg, name, value, nil +} + func link(args []string) error { // Parse arguments. args, _, err := expandParamsFiles(args) @@ -50,6 +126,9 @@ func link(args []string) error { buildmode := flags.String("buildmode", "", "Build mode used.") flags.Var(&xdefs, "X", "A string variable to replace in the linked binary (repeated).") flags.Var(&stamps, "stamp", "The name of a file with stamping values.") + buildinfoFile := flags.String("buildinfo", "", "Path to buildinfo dependency file for Go 1.18+ buildInfo.") + versionMapFile := flags.String("versionmap", "", "Path to version map file with real dependency versions from package_info.") + bazelTarget := flags.String("bazeltarget", "", "Bazel target label for buildInfo metadata.") if err := flags.Parse(builderArgs); err != nil { return err } @@ -70,9 +149,9 @@ func link(args []string) error { // If we were given any stamp value files, read and parse them stampMap := map[string]string{} for _, stampfile := range stamps { - stampbuf, err := ioutil.ReadFile(stampfile) + stampbuf, err := os.ReadFile(stampfile) if err != nil { - return fmt.Errorf("Failed reading stamp file %s: %v", stampfile, err) + return fmt.Errorf("reading stamp file %s: %w", stampfile, err) } scanner := bufio.NewScanner(bytes.NewReader(stampbuf)) for scanner.Scan() { @@ -90,8 +169,167 @@ func link(args []string) error { } } + // Parse version map file if provided (real versions from package_info) + versionMap := make(map[string]string) + if *versionMapFile != "" { + versionMapData, err := os.ReadFile(*versionMapFile) + if err != nil { + return fmt.Errorf("reading version map file %s: %w", *versionMapFile, err) + } + + // Parse the version map file: importpath\tversion + scanner := bufio.NewScanner(bytes.NewReader(versionMapData)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + parts := strings.Split(line, "\t") + if len(parts) >= 2 { + importpath := strings.TrimSpace(parts[0]) + version := strings.TrimSpace(parts[1]) + // Validate that both importpath and version are non-empty + if importpath == "" || version == "" { + continue + } + versionMap[importpath] = version + } + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("scanning version map file %s: %w", *versionMapFile, err) + } + } + + // Parse buildinfo file if provided (for Go 1.18+ dependency metadata) + // Merge with version map to replace v0.0.0 with real versions + var deps []*Module + if *buildinfoFile != "" { + buildinfoData, err := os.ReadFile(*buildinfoFile) + if err != nil { + return fmt.Errorf("reading buildinfo file %s: %w", *buildinfoFile, err) + } + + // Pre-compute sorted list of module paths for efficient lookup + // This enables O(M) lookup per dependency instead of O(M) for each + sortedModules := make([]string, 0, len(versionMap)) + for modulePath := range versionMap { + sortedModules = append(sortedModules, modulePath) + } + + // Parse the buildinfo file to extract dependency information + // Format: tab-separated lines with "path", "dep", etc. + // Single-pass parsing for both main module path and dependencies + mainModulePath := "" + scanner := bufio.NewScanner(bytes.NewReader(buildinfoData)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + parts := strings.Split(line, "\t") + + // Extract main module path + if len(parts) >= 2 && parts[0] == "path" { + mainModulePath = strings.TrimSpace(parts[1]) + continue + } + + // Collect dependencies + if len(parts) >= 3 && parts[0] == "dep" { + importpath := strings.TrimSpace(parts[1]) + version := strings.TrimSpace(parts[2]) + + // Validate that importpath and version are non-empty + if importpath == "" || version == "" { + continue + } + + // Skip internal packages (from main module) + // Filter out packages that are part of the main module being built + // Check both exact match and prefix to handle subpackages + if mainModulePath != "" && (importpath == mainModulePath || strings.HasPrefix(importpath, mainModulePath+"/")) { + continue + } + + // Replace (devel) sentinel with real version from version map + if version == "(devel)" { + // First try exact match + if realVersion, ok := versionMap[importpath]; ok && realVersion != "" { + version = realVersion + } else { + // Try to find parent module version for subpackages + // Use longest-prefix matching to find module root + if bestMatch := findBestModuleMatch(importpath, sortedModules); bestMatch != "" { + version = versionMap[bestMatch] + } + } + } + + // Skip dependencies that still have (devel) after version resolution + // These are internal packages from the monorepo without real versions + // (devel) is an invalid semantic version used as a sentinel + if version == "(devel)" { + continue + } + + // Format: dep\t\t + deps = append(deps, &Module{ + Path: importpath, + Version: version, + Sum: "", // No checksum in Bazel builds + }) + } + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("scanning buildinfo file %s: %w", *buildinfoFile, err) + } + } + + // Prepare link config for buildinfo generation + var realBuildMode string + if *buildmode == "" { + realBuildMode = "exe" + } else { + realBuildMode = *buildmode + } + + cgoEnabled := os.Getenv("CGO_ENABLED") == "1" + goarch := os.Getenv("GOARCH") + goos := os.Getenv("GOOS") + + // Extract architecture feature level + goarchFeatureKey, goarchFeatureValue := getArchFeature(goarch) + + // Extract CGO flags + cgoCflags := "" + cgoCxxflags := "" + cgoLdflags := "" + if cgoEnabled { + cgoCflags = os.Getenv("CGO_CFLAGS") + cgoCxxflags = os.Getenv("CGO_CXXFLAGS") + cgoLdflags = os.Getenv("CGO_LDFLAGS") + } + + cfg := linkConfig{ + path: *packagePath, + buildMode: realBuildMode, + compiler: "gc", + cgoEnabled: cgoEnabled, + goos: goos, + goarch: goarch, + pgoProfilePath: "", // Will be set below if pgoprofile is provided + buildinfoFile: *buildinfoFile, + deps: deps, + goarchFeatureKey: goarchFeatureKey, + goarchFeatureValue: goarchFeatureValue, + cgoCflags: cgoCflags, + cgoCxxflags: cgoCxxflags, + cgoLdflags: cgoLdflags, + bazelTarget: *bazelTarget, + } + // Build an importcfg file. - importcfgName, err := buildImportcfgFileForLink(archives, *packageList, goenv.installSuffix, filepath.Dir(*outFile)) + importcfgName, err := buildImportcfgFileForLink(archives, *packageList, goenv.installSuffix, filepath.Dir(*outFile), cfg) if err != nil { return err } @@ -103,23 +341,8 @@ func link(args []string) error { goargs := goenv.goTool("link") goargs = append(goargs, "-importcfg", importcfgName) - parseXdef := func(xdef string) (pkg, name, value string, err error) { - eq := strings.IndexByte(xdef, '=') - if eq < 0 { - return "", "", "", fmt.Errorf("-X flag does not contain '=': %s", xdef) - } - dot := strings.LastIndexByte(xdef[:eq], '.') - if dot < 0 { - return "", "", "", fmt.Errorf("-X flag does not contain '.': %s", xdef) - } - pkg, name, value = xdef[:dot], xdef[dot+1:eq], xdef[eq+1:] - if pkg == *packagePath { - pkg = "main" - } - return pkg, name, value, nil - } for _, xdef := range xdefs { - pkg, name, value, err := parseXdef(xdef) + pkg, name, value, err := parseXdef(xdef, *packagePath) if err != nil { return err } @@ -176,7 +399,7 @@ func link(args []string) error { if *buildmode == "c-archive" { if err := stripArMetadata(*outFile); err != nil { - return fmt.Errorf("error stripping archive metadata: %v", err) + return fmt.Errorf("error stripping archive metadata: %w", err) } } diff --git a/go/tools/builders/modinfo.go b/go/tools/builders/modinfo.go new file mode 100644 index 0000000000..23467302a6 --- /dev/null +++ b/go/tools/builders/modinfo.go @@ -0,0 +1,34 @@ +// FROM https://github.com/golang/go/blob/f4e37b8afc01253567fddbdd68ec35632df86b62/src/cmd/go/internal/modload/build.go +package main + +import ( + "encoding/hex" +) + +// Magic numbers for the buildinfo. These byte sequences are used by the Go +// runtime to locate embedded build information in binaries. +// First added in https://go-review.googlesource.com/c/go/+/123576/4/src/cmd/go/internal/modload/build.go +var ( + infoStart []byte + infoEnd []byte +) + +func init() { + var err error + infoStart, err = hex.DecodeString("3077af0c9274080241e1c107e6d618e6") + if err != nil { + panic("invalid infoStart hex string: " + err.Error()) + } + infoEnd, err = hex.DecodeString("f932433186182072008242104116d8f2") + if err != nil { + panic("invalid infoEnd hex string: " + err.Error()) + } +} + +// ModInfoData wraps build information with Go's internal magic markers. +// These markers enable the Go runtime to locate and extract build metadata +// via runtime/debug.ReadBuildInfo(). The info string should be formatted +// according to Go's buildinfo text format. +func ModInfoData(info string) []byte { + return []byte(string(infoStart) + info + string(infoEnd)) +} diff --git a/tests/core/buildinfo/BUILD.bazel b/tests/core/buildinfo/BUILD.bazel new file mode 100644 index 0000000000..04cf626cfc --- /dev/null +++ b/tests/core/buildinfo/BUILD.bazel @@ -0,0 +1,69 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +# Test suite aggregation +test_suite( + name = "buildinfo", + tests = [ + ":external_deps_test", + ":metadata_test", + ], +) + +# Test libraries with a dependency chain +go_library( + name = "leaf_lib", + srcs = ["leaf_lib.go"], + importpath = "github.com/bazelbuild/rules_go/tests/core/buildinfo/leaf", +) + +go_library( + name = "mid_lib", + srcs = ["mid_lib.go"], + importpath = "github.com/bazelbuild/rules_go/tests/core/buildinfo/mid", + deps = [":leaf_lib"], +) + +go_library( + name = "top_lib", + srcs = ["top_lib.go"], + importpath = "github.com/bazelbuild/rules_go/tests/core/buildinfo/top", + deps = [":mid_lib"], +) + +# Binary that uses the libraries +go_binary( + name = "metadata_bin", + srcs = ["metadata_bin.go"], + deps = [":top_lib"], +) + +# Runtime test that validates buildinfo metadata +go_test( + name = "metadata_test", + srcs = ["metadata_test.go"], + data = [":metadata_bin"], + rundir = ".", + deps = ["@io_bazel_rules_go//go/tools/bazel:go_default_library"], +) + +# Binary with external dependencies +go_binary( + name = "external_deps_bin", + srcs = [ + "external_deps_bin_unix.go", + "external_deps_bin_windows.go", + ], + deps = [ + "@org_golang_x_sys//unix", + "@org_golang_x_sys//windows", + ], +) + +# Test that validates external dependency metadata +go_test( + name = "external_deps_test", + srcs = ["external_deps_test.go"], + data = [":external_deps_bin"], + rundir = ".", + deps = ["@io_bazel_rules_go//go/tools/bazel:go_default_library"], +) diff --git a/tests/core/buildinfo/README.md b/tests/core/buildinfo/README.md new file mode 100644 index 0000000000..b61d4cd048 --- /dev/null +++ b/tests/core/buildinfo/README.md @@ -0,0 +1,93 @@ +# BuildInfo Metadata Tests + +This directory contains tests for the buildInfo metadata changes that implement Go 1.18+ runtime/debug.BuildInfo embedding with dependency version information. + +## Test Structure + +### Runtime Integration Tests + +1. **metadata_test.go** - Tests internal dependency version collection + - Builds a binary with a chain of internal dependencies (leaf_lib -> mid_lib -> top_lib) + - Validates that `runtime/debug.ReadBuildInfo()` returns build information + - Checks that all transitive Go dependencies are listed in the BuildInfo + - Verifies the main package path is set correctly + +2. **external_deps_test.go** - Tests external dependency version collection + - Builds a binary that depends on golang.org/x/sys + - Validates that external dependencies appear in BuildInfo + - Checks that real versions (not "(devel)") are used for external dependencies with package_metadata + +## Implementation Changes + +The tests cover the refactored buildInfo implementation that addresses review comments: + +### Key Changes from Committed Version (ea1600b9) + +1. **buildinfo_aspect.bzl** - Changed provider schema: + - OLD: Single `version_map` depset of (importpath, version) tuples + - NEW: Separate `importpaths` and `metadata_providers` depsets + - Avoids quadratic memory usage by deferring version materialization to link time + - Only traverses `deps` and `embed` attributes instead of all attributes + +2. **archive.bzl** - Removed quadratic aggregation: + - OLD: Collected `_buildinfo_deps` tuples in each archive + - NEW: No buildinfo aggregation in archives + - BuildInfo metadata now collected only via aspect + +3. **link.bzl** - Deferred materialization: + - OLD: Received pre-computed version tuples from archives + - NEW: Receives BuildInfoMetadata provider and materializes depsets at link time + - Extracts versions from package_metadata providers on-demand + +4. **binary.bzl** - Simplified metadata passing: + - OLD: Created version_map file by collecting and deduplicating tuples + - NEW: Passes BuildInfoMetadata provider directly to link action + +## Test Files + +``` +tests/core/buildinfo/ +├── BUILD.bazel # Test target definitions +├── README.md # This file +├── leaf_lib.go # Bottom of dependency chain +├── mid_lib.go # Middle of dependency chain +├── top_lib.go # Top of dependency chain +├── metadata_bin.go # Binary that prints BuildInfo +├── metadata_test.go # Runtime test for internal deps +├── external_deps_bin.go # Binary with external deps +└── external_deps_test.go # Runtime test for external deps +``` + +## Running the Tests + +```bash +# Run all buildinfo tests +bazel test //tests/core/buildinfo:buildinfo + +# Run individual tests +bazel test //tests/core/buildinfo:metadata_test +bazel test //tests/core/buildinfo:external_deps_test +``` + +## Expected Behavior + +### Internal Dependencies (metadata_test) + +The test validates that binaries built with rules_go embed proper BuildInfo including: +- Main package path +- Go version +- All transitive Go dependencies with their import paths +- For internal monorepo packages: version shows as "(devel)" + +### External Dependencies (external_deps_test) + +The test validates that external dependencies from go_repository: +- Are included in the dependency list +- Have real version strings (e.g., "v0.1.0") instead of "(devel)" +- Version information comes from package_metadata set in REPO.bazel + +## Notes + +- The BuildInfoMetadata provider is internal to the binary rule implementation and not exposed on targets +- The aspect is automatically applied to go_binary targets via the deps and embed attributes +- Tests focus on runtime behavior validation via `runtime/debug.ReadBuildInfo()` diff --git a/tests/core/buildinfo/external_deps_bin_unix.go b/tests/core/buildinfo/external_deps_bin_unix.go new file mode 100644 index 0000000000..6c2f59445f --- /dev/null +++ b/tests/core/buildinfo/external_deps_bin_unix.go @@ -0,0 +1,29 @@ +//go:build unix + +package main + +import ( + "fmt" + "runtime/debug" + + "golang.org/x/sys/unix" +) + +func main() { + // Use the external dependency + _ = unix.ENOENT + + // Print buildInfo for testing + info, ok := debug.ReadBuildInfo() + if !ok { + fmt.Println("NO_BUILD_INFO") + return + } + + fmt.Printf("Path=%s\n", info.Path) + + // Print dependencies in sorted order + for _, dep := range info.Deps { + fmt.Printf("Dep=%s@%s\n", dep.Path, dep.Version) + } +} diff --git a/tests/core/buildinfo/external_deps_bin_windows.go b/tests/core/buildinfo/external_deps_bin_windows.go new file mode 100644 index 0000000000..0d7d38f122 --- /dev/null +++ b/tests/core/buildinfo/external_deps_bin_windows.go @@ -0,0 +1,29 @@ +//go:build windows + +package main + +import ( + "fmt" + "runtime/debug" + + "golang.org/x/sys/windows" +) + +func main() { + // Use the external dependency + _ = windows.ERROR_FILE_NOT_FOUND + + // Print buildInfo for testing + info, ok := debug.ReadBuildInfo() + if !ok { + fmt.Println("NO_BUILD_INFO") + return + } + + fmt.Printf("Path=%s\n", info.Path) + + // Print dependencies in sorted order + for _, dep := range info.Deps { + fmt.Printf("Dep=%s@%s\n", dep.Path, dep.Version) + } +} diff --git a/tests/core/buildinfo/external_deps_test.go b/tests/core/buildinfo/external_deps_test.go new file mode 100644 index 0000000000..7ef8124f7d --- /dev/null +++ b/tests/core/buildinfo/external_deps_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "os/exec" + "regexp" + "strings" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel" +) + +const ( + noBuildInfoMarker = "NO_BUILD_INFO" + externalDepModule = "golang.org/x/sys" +) + +func TestExternalDeps(t *testing.T) { + // Run binary once and share output across subtests + bin, ok := bazel.FindBinary("tests/core/buildinfo", "external_deps_bin") + if !ok { + t.Fatal("could not find external_deps_bin") + } + + out, err := exec.Command(bin).CombinedOutput() + if err != nil { + t.Fatalf("failed to run external_deps_bin: %v\noutput: %s", err, out) + } + + output := string(out) + t.Logf("external_deps_bin output:\n%s", output) + + t.Run("BuildInfoPresent", func(t *testing.T) { + if strings.Contains(output, noBuildInfoMarker) { + t.Errorf("BuildInfo check failed: output contains %q marker, binary should have build info embedded", noBuildInfoMarker) + } + }) + + t.Run("ExternalDependencyListed", func(t *testing.T) { + // Check that the external dependency is listed + expectedDep := "Dep=" + externalDepModule + if !strings.Contains(output, expectedDep) { + t.Errorf("ExternalDependencyListed check: output missing %q\nGot output: %s", expectedDep, output) + } + }) + + t.Run("ExternalDependencyVersion", func(t *testing.T) { + // Version should be set (not (devel) for external dependencies) + // This validates that the aspect collected package_metadata correctly + lines := strings.Split(output, "\n") + foundSysDep := false + versionPattern := regexp.MustCompile(`^v\d+\.\d+\.\d+`) + + for _, line := range lines { + if strings.HasPrefix(line, "Dep="+externalDepModule+"@") { + foundSysDep = true + version := strings.TrimPrefix(line, "Dep="+externalDepModule+"@") + + if version == "(devel)" { + t.Errorf("ExternalDependencyVersion check for %s: got version=(devel), want real version (format: v0.0.0)\nFull line: %q", externalDepModule, line) + } else if !versionPattern.MatchString(version) { + t.Errorf("ExternalDependencyVersion check for %s: got malformed version=%q, want format v0.0.0\nFull line: %q", externalDepModule, version, line) + } else { + t.Logf("Found %s with valid version: %s", externalDepModule, version) + } + } + } + + if !foundSysDep { + t.Errorf("ExternalDependencyVersion check: %s dependency not found in output\nSearched for pattern: Dep=%s@\nGot output: %s", + externalDepModule, externalDepModule, output) + } + }) +} diff --git a/tests/core/buildinfo/leaf_lib.go b/tests/core/buildinfo/leaf_lib.go new file mode 100644 index 0000000000..2e44a26e0e --- /dev/null +++ b/tests/core/buildinfo/leaf_lib.go @@ -0,0 +1,6 @@ +package leaf + +// LeafFunc is a simple function in the leaf library +func LeafFunc() string { + return "leaf" +} diff --git a/tests/core/buildinfo/metadata_bin.go b/tests/core/buildinfo/metadata_bin.go new file mode 100644 index 0000000000..42f3a15561 --- /dev/null +++ b/tests/core/buildinfo/metadata_bin.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "runtime/debug" + + "github.com/bazelbuild/rules_go/tests/core/buildinfo/top" +) + +func main() { + fmt.Println(top.TopFunc()) + + // Print buildInfo for testing + info, ok := debug.ReadBuildInfo() + if !ok { + fmt.Println("NO_BUILD_INFO") + return + } + + fmt.Printf("Path=%s\n", info.Path) + fmt.Printf("GoVersion=%s\n", info.GoVersion) + + // Print dependencies in sorted order + for _, dep := range info.Deps { + fmt.Printf("Dep=%s@%s\n", dep.Path, dep.Version) + } +} diff --git a/tests/core/buildinfo/metadata_test.go b/tests/core/buildinfo/metadata_test.go new file mode 100644 index 0000000000..bc203a2c61 --- /dev/null +++ b/tests/core/buildinfo/metadata_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "os/exec" + "regexp" + "strings" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel" +) + +const ( + noBuildInfoMarker = "NO_BUILD_INFO" + expectedMainPath = "github.com/bazelbuild/rules_go/tests/core/buildinfo" +) + +func TestMetadata(t *testing.T) { + // Run binary once and share output across subtests + bin, ok := bazel.FindBinary("tests/core/buildinfo", "metadata_bin") + if !ok { + t.Fatal("could not find metadata_bin") + } + + out, err := exec.Command(bin).CombinedOutput() + if err != nil { + t.Fatalf("failed to run metadata_bin: %v\noutput: %s", err, out) + } + + output := string(out) + t.Logf("metadata_bin output:\n%s", output) + + t.Run("BuildInfoPresent", func(t *testing.T) { + if strings.Contains(output, noBuildInfoMarker) { + t.Errorf("BuildInfo check failed: output contains %q marker, binary should have build info embedded\nGot output: %s", noBuildInfoMarker, output) + } + }) + + t.Run("MainPackagePath", func(t *testing.T) { + expectedPath := "Path=" + expectedMainPath + if !strings.Contains(output, expectedPath) { + t.Errorf("MainPackagePath check: output missing %q\nGot output: %s", expectedPath, output) + } + }) + + t.Run("GoVersion", func(t *testing.T) { + // Validate that Go version is present and well-formed (e.g., go1.20.1) + goVersionPattern := regexp.MustCompile(`GoVersion=go\d+\.\d+`) + if !goVersionPattern.MatchString(output) { + t.Errorf("GoVersion check: output missing valid Go version pattern (expected GoVersion=go1.20.x format)\nGot output: %s", output) + } + }) + + t.Run("TransitiveDependencies", func(t *testing.T) { + // Check that all transitive Go dependencies are listed + expectedDeps := []string{ + "github.com/bazelbuild/rules_go/tests/core/buildinfo/leaf", + "github.com/bazelbuild/rules_go/tests/core/buildinfo/mid", + "github.com/bazelbuild/rules_go/tests/core/buildinfo/top", + } + + missingDeps := []string{} + for _, dep := range expectedDeps { + if !strings.Contains(output, "Dep="+dep) { + missingDeps = append(missingDeps, dep) + } + } + if len(missingDeps) > 0 { + t.Errorf("TransitiveDependencies check: missing %d dependencies: %v\nGot output: %s", + len(missingDeps), missingDeps, output) + } + }) +} diff --git a/tests/core/buildinfo/mid_lib.go b/tests/core/buildinfo/mid_lib.go new file mode 100644 index 0000000000..fc8b68ee4a --- /dev/null +++ b/tests/core/buildinfo/mid_lib.go @@ -0,0 +1,8 @@ +package mid + +import "github.com/bazelbuild/rules_go/tests/core/buildinfo/leaf" + +// MidFunc calls the leaf library +func MidFunc() string { + return "mid-" + leaf.LeafFunc() +} diff --git a/tests/core/buildinfo/top_lib.go b/tests/core/buildinfo/top_lib.go new file mode 100644 index 0000000000..15f7e2fefe --- /dev/null +++ b/tests/core/buildinfo/top_lib.go @@ -0,0 +1,8 @@ +package top + +import "github.com/bazelbuild/rules_go/tests/core/buildinfo/mid" + +// TopFunc calls the mid library +func TopFunc() string { + return "top-" + mid.MidFunc() +}