diff --git a/MODULE.bazel b/MODULE.bazel index 38438955ad..b447c1994d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -13,6 +13,12 @@ bazel_dep(name = "rules_proto", version = "7.0.2") bazel_dep(name = "protobuf", version = "29.0", repo_name = "com_google_protobuf") bazel_dep(name = "rules_shell", version = "0.3.0") bazel_dep(name = "rules_cc", version = "0.1.5") +bazel_dep(name = "rules_license", version = "1.0.0") +single_version_override( + module_name = "rules_license", + patch_strip = 1, + patches = ["//third_party:rules_license-stardoc.patch"], +) go_sdk = use_extension("//go:extensions.bzl", "go_sdk") @@ -32,7 +38,7 @@ use_repo( register_toolchains("@go_toolchains//:all") -bazel_dep(name = "gazelle", version = "0.36.0") +bazel_dep(name = "gazelle", version = "0.47.0") go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_mod = "//:go.mod") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index baae45c732..bd694623ad 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -43,8 +43,8 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953", "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", - "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", - "https://bcr.bazel.build/modules/gazelle/0.36.0/source.json": "0823f097b127e0201ae55d85647c94095edfe27db0431a7ae880dcab08dfaa04", + "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", + "https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", "https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/MODULE.bazel": "97c6a4d413b373d4cc97065da3de1b2166e22cbbb5f4cc9f05760bfa83619e24", "https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/source.json": "cf611c836a60e98e2e2ab2de8004f119e9f06878dcf4ea2d95a437b1b7a89fe9", @@ -57,6 +57,8 @@ "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d", "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index d4742ad53f..4e05e96d53 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -3,11 +3,19 @@ load("//docs:doc_helpers.bzl", "stardoc_with_diff_test", "update_docs") # For each doc file, generate MD from bzl_library, then perform diff test stardoc_with_diff_test( bzl_library_target = "//docs/go/extras:extras", + extra_deps = [ + "@rules_license//rules:providers.bzl", + "@rules_license//rules_gathering:gathering_providers.bzl", + ], out_label = "//docs/go/extras:extras.md", ) stardoc_with_diff_test( bzl_library_target = "//docs/go/core:rules", + extra_deps = [ + "@rules_license//rules:providers.bzl", + "@rules_license//rules_gathering:gathering_providers.bzl", + ], out_label = "//docs/go/core:rules.md", ) diff --git a/docs/doc_helpers.bzl b/docs/doc_helpers.bzl index 058827a96d..15473a3265 100644 --- a/docs/doc_helpers.bzl +++ b/docs/doc_helpers.bzl @@ -14,12 +14,13 @@ load("@bazel_skylib//rules:diff_test.bzl", "diff_test") load("@bazel_skylib//rules:write_file.bzl", "write_file") -load("@stardoc//stardoc:stardoc.bzl", "stardoc") load("@rules_shell//shell:sh_binary.bzl", "sh_binary") +load("@stardoc//stardoc:stardoc.bzl", "stardoc") def stardoc_with_diff_test( bzl_library_target, - out_label): + out_label, + extra_deps = []): """Creates a stardoc target coupled with a diff_test for a given bzl_library. This is helpful for minimizing boilerplate when lots of stardoc targets are to be generated. @@ -27,6 +28,7 @@ def stardoc_with_diff_test( Args: bzl_library_target: the label of the bzl_library target to generate documentation for out_label: the label of the output MD file + extra_deps: additional Starlark files or bzl_library targets required for doc extraction """ out_file = out_label.replace("//", "").replace(":", "/") @@ -36,7 +38,7 @@ def stardoc_with_diff_test( name = out_file.replace("/", "_").replace(".md", "-docgen"), out = out_file.replace(".md", "-docgen.md"), input = bzl_library_target + ".bzl", - deps = [bzl_library_target], + deps = [bzl_library_target] + extra_deps, ) # Ensure that the generated MD has been updated in the local source tree diff --git a/go/private/actions/archive.bzl b/go/private/actions/archive.bzl index fa4daad51e..dd53cefa96 100644 --- a/go/private/actions/archive.bzl +++ b/go/private/actions/archive.bzl @@ -193,6 +193,8 @@ def emit_archive(go, source = None, _recompile_suffix = "", recompile_internal_d _copts = tuple(source.copts), _cxxopts = tuple(source.cxxopts), _clinkopts = tuple(source.clinkopts), + _module_path = getattr(source, "_module_path", ""), + _module_version = getattr(source, "_module_version", ""), # Information on dependencies _dep_labels = tuple([d.data.label for d in direct]), @@ -227,4 +229,6 @@ def emit_archive(go, source = None, _recompile_suffix = "", recompile_internal_d cgo_exports = cgo_exports, runfiles = runfiles, _headers = headers, + _module_path = getattr(data, "_module_path", ""), + _module_version = getattr(data, "_module_version", ""), ) diff --git a/go/private/actions/link.bzl b/go/private/actions/link.bzl index 7d8ff5f0f6..fc88eed241 100644 --- a/go/private/actions/link.bzl +++ b/go/private/actions/link.bzl @@ -35,7 +35,14 @@ load( ) def _format_archive(d): - return "{}={}={}".format(d.label, d.importmap, d.file.path) + return "{}={}={}={}={}={}".format( + d.label, + d.importpath, + d.importmap, + d.file.path, + getattr(d, "_module_path", ""), + getattr(d, "_module_version", ""), + ) def emit_link( go, @@ -125,6 +132,7 @@ def emit_link( arcs = depset(test_archives, transitive = [d.transitive for d in archive.direct]) builder_args.add_all(arcs, before_each = "-arc", map_each = _format_archive) + builder_args.add("-go_version", go.sdk.version) builder_args.add("-package_list", go.sdk.package_list) # Build a list of rpaths for dynamic libraries we need to find. diff --git a/go/private/context.bzl b/go/private/context.bzl index 0bd1734ea9..12147614a5 100644 --- a/go/private/context.bzl +++ b/go/private/context.bzl @@ -40,6 +40,7 @@ load( NOGO_INCLUDES = "INCLUDES", ) load("@rules_cc//cc/common:cc_common.bzl", "cc_common") +load("@rules_license//rules:providers.bzl", "PackageInfo") load( "//go/platform:apple.bzl", "apple_ensure_options", @@ -240,6 +241,35 @@ def _tool_args(go): args.use_param_file("-param=%s") return args +def normalize_module_version(version): + if not version: + return "(devel)" + if version == "(devel)": + return version + if version.startswith("v"): + return version + return "v" + version + +def module_info_from_metadata(label, package_metadata = (), applicable_licenses = ()): + if not label.repo_name and not label.workspace_root: + return struct(path = "", version = "") + + # Bazel may surface repo-level metadata through either spelling depending on + # the version and rule surface, so probe both. + for metadata_group in (package_metadata, applicable_licenses): + for metadata in metadata_group: + if PackageInfo not in metadata: + continue + info = metadata[PackageInfo] + if not info.package_name: + continue + return struct( + path = info.package_name, + version = normalize_module_version(info.package_version), + ) + + return struct(path = "", version = "") + def _merge_embed(source, embed): s = get_source(embed) source["srcs"] = s.srcs + source["srcs"] @@ -249,6 +279,10 @@ def _merge_embed(source, embed): source["x_defs"].update(s.x_defs) source["gc_goopts"] = source["gc_goopts"] + s.gc_goopts source["runfiles"] = source["runfiles"].merge(s.runfiles) + module_path = getattr(s, "_module_path", "") + if not source["_module_path"] and module_path: + source["_module_path"] = module_path + source["_module_version"] = getattr(s, "_module_version", "") if s.cgo: if source["cgo"]: @@ -354,6 +388,12 @@ def new_go_info( if deps == None: deps = [get_archive(dep) for dep in getattr(attr, "deps", [])] + module_info = module_info_from_metadata( + go.label, + getattr(attr, "package_metadata", ()), + getattr(attr, "applicable_licenses", ()), + ) + go_info = { "name": go.label.name if not name else name, "label": go.label, @@ -378,6 +418,8 @@ def new_go_info( "cxxopts": _expand_opts(go, "cxxopts", getattr(attr, "cxxopts", [])), "clinkopts": _expand_opts(go, "clinkopts", getattr(attr, "clinkopts", [])), "pgoprofile": getattr(attr, "pgoprofile", None), + "_module_path": module_info.path, + "_module_version": module_info.version, } for e in getattr(attr, "embed", []): diff --git a/go/private/repositories.bzl b/go/private/repositories.bzl index 94adead5c1..2b81eb5640 100644 --- a/go/private/repositories.bzl +++ b/go/private/repositories.bzl @@ -341,6 +341,17 @@ def go_rules_dependencies(force = False): url = "https://github.com/bazelbuild/rules_cc/releases/download/0.1.5/rules_cc-0.1.5.tar.gz", ) + # Required for reading Gazelle-generated PackageInfo metadata. + wrapper( + http_archive, + name = "rules_license", + sha256 = "26d4021f6898e23b82ef953078389dd49ac2b5618ac564ade4ef87cced147b38", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_license/releases/download/1.0.0/rules_license-1.0.0.tar.gz", + "https://github.com/bazelbuild/rules_license/releases/download/1.0.0/rules_license-1.0.0.tar.gz", + ], + ) + def _go_host_compatible_sdk_label_impl(ctx): ctx.file("BUILD.bazel") ctx.file("defs.bzl", """HOST_COMPATIBLE_SDK = Label("@go_sdk//:ROOT")""") diff --git a/go/private/rules/test.bzl b/go/private/rules/test.bzl index 911a92496d..5c0d883600 100644 --- a/go/private/rules/test.bzl +++ b/go/private/rules/test.bzl @@ -727,6 +727,8 @@ def _recompile_external_deps(go, external_go_info, internal_archive, library_lab copts = list(arc_data._copts), cxxopts = list(arc_data._cxxopts), clinkopts = list(arc_data._clinkopts), + _module_path = getattr(arc_data, "_module_path", ""), + _module_version = getattr(arc_data, "_module_version", ""), ) # If this archive needs to be recompiled, use go.archive. @@ -747,6 +749,8 @@ def _recompile_external_deps(go, external_go_info, internal_archive, library_lab runfiles = go_info.runfiles, mode = go.mode, _headers = internal_archive._headers, + _module_path = getattr(arc_data, "_module_path", ""), + _module_version = getattr(arc_data, "_module_version", ""), ) label_to_archive[label] = archive diff --git a/go/tools/builders/BUILD.bazel b/go/tools/builders/BUILD.bazel index 9665e29bd3..3ebd29d5de 100644 --- a/go/tools/builders/BUILD.bazel +++ b/go/tools/builders/BUILD.bazel @@ -80,6 +80,15 @@ go_test( ], ) +go_test( + name = "buildinfo_test", + size = "small", + srcs = [ + "buildinfo.go", + "buildinfo_test.go", + ], +) + go_test( name = "nogo_version_test", size = "small", @@ -101,6 +110,7 @@ filegroup( "ar.go", "asm.go", "builder.go", + "buildinfo.go", "cc.go", "cgo2.go", "compilepkg.go", diff --git a/go/tools/builders/buildinfo.go b/go/tools/builders/buildinfo.go new file mode 100644 index 0000000000..034435a188 --- /dev/null +++ b/go/tools/builders/buildinfo.go @@ -0,0 +1,129 @@ +// Copyright 2026 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 ( + "runtime/debug" + "sort" + "strconv" + "strings" +) + +const ( + // Match cmd/go's modinfo framing markers: + // https://go.dev/src/cmd/go/internal/modload/build.go#L29-L30 + buildInfoStart = "\x30\x77\xaf\x0c\x92\x74\x08\x02\x41\xe1\xc1\x07\xe6\xd6\x18\xe6" + buildInfoEnd = "\xf9\x32\x43\x31\x86\x18\x20\x72\x00\x82\x42\x10\x41\x16\xd8\xf2" +) + +type moduleInfo struct { + path string + version string +} + +func buildInfoDeps(modules []moduleInfo) []*debug.Module { + seen := map[moduleInfo]struct{}{} + unique := make([]moduleInfo, 0, len(modules)) + for _, module := range modules { + if module.path == "" || module.version == "" { + continue + } + if _, ok := seen[module]; ok { + continue + } + seen[module] = struct{}{} + unique = append(unique, module) + } + + sort.Slice(unique, func(i, j int) bool { + if unique[i].path != unique[j].path { + return unique[i].path < unique[j].path + } + return unique[i].version < unique[j].version + }) + + deps := make([]*debug.Module, 0, len(unique)) + for _, module := range unique { + deps = append(deps, &debug.Module{ + Path: module.path, + Version: module.version, + }) + } + return deps +} + +func modInfoData(modules []moduleInfo) string { + deps := buildInfoDeps(modules) + if len(deps) == 0 { + return buildInfoStart + buildInfoEnd + } + + // debug.BuildInfo.String was added after Go 1.17. Emit the dep-only + // modinfo format directly so older SDK builders still compile. + var buf strings.Builder + for _, dep := range deps { + buf.WriteString("dep\t") + buf.WriteString(dep.Path) + buf.WriteByte('\t') + buf.WriteString(dep.Version) + buf.WriteString("\t\n") + } + return buildInfoStart + buf.String() + buildInfoEnd +} + +func shouldEmitBuildInfo(goVersion, buildmode string) bool { + switch buildmode { + case "c-archive", "c-shared", "plugin": + return false + default: + return supportsBuildInfo(goVersion) + } +} + +func supportsBuildInfo(goVersion string) bool { + if goVersion == "" { + return true + } + goVersion = strings.TrimPrefix(goVersion, "go") + parts := strings.SplitN(goVersion, ".", 3) + if len(parts) != 2 { + if len(parts) == 3 { + parts = parts[:2] + } else { + // Keep build info enabled for newer or non-standard version strings + // we don't recognize. + return true + } + } + minor := parts[1] + for i := 0; i < len(minor); i++ { + if minor[i] < '0' || minor[i] > '9' { + minor = minor[:i] + break + } + } + if minor == "" { + return true + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + return true + } + minorVersion, err := strconv.Atoi(minor) + if err != nil { + return true + } + return major > 1 || (major == 1 && minorVersion >= 18) +} diff --git a/go/tools/builders/buildinfo_test.go b/go/tools/builders/buildinfo_test.go new file mode 100644 index 0000000000..fb192296cd --- /dev/null +++ b/go/tools/builders/buildinfo_test.go @@ -0,0 +1,137 @@ +// Copyright 2026 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 ( + "runtime/debug" + "slices" + "strings" + "testing" +) + +func parseModInfoData(t *testing.T, data string) *debug.BuildInfo { + t.Helper() + + info, found := strings.CutPrefix(data, buildInfoStart) + if !found { + t.Fatalf("modinfo missing start marker: %q", data) + } + info, found = strings.CutSuffix(info, buildInfoEnd) + if !found { + t.Fatalf("modinfo missing end marker: %q", data) + } + + parsed, err := debug.ParseBuildInfo(info) + if err != nil { + t.Fatalf("ParseBuildInfo(%q): %v", info, err) + } + return parsed +} + +func TestBuildInfoDepsSortAndDedup(t *testing.T) { + deps := buildInfoDeps([]moduleInfo{ + {path: "golang.org/x/text", version: "v0.15.0"}, + {path: "github.com/google/go-cmp", version: "v0.6.0"}, + {path: "golang.org/x/text", version: "v0.15.0"}, + {path: "", version: "v1.0.0"}, + {path: "example.com/missing/version", version: ""}, + }) + + got := make([]string, 0, len(deps)) + for _, dep := range deps { + got = append(got, dep.Path+"@"+dep.Version) + } + want := []string{ + "github.com/google/go-cmp@v0.6.0", + "golang.org/x/text@v0.15.0", + } + if !slices.Equal(got, want) { + t.Fatalf("got deps %v; want %v", got, want) + } +} + +func TestModInfoDataRoundTrip(t *testing.T) { + info := parseModInfoData(t, modInfoData([]moduleInfo{ + {path: "golang.org/x/sync", version: "v0.8.0"}, + {path: "github.com/google/go-cmp", version: "v0.6.0"}, + {path: "github.com/google/go-cmp", version: "v0.6.0"}, + })) + + if info.Path != "" { + t.Fatalf("got Path %q; want empty", info.Path) + } + if info.Main.Path != "" || info.Main.Version != "" { + t.Fatalf("got Main %+v; want empty", info.Main) + } + + got := make([]string, 0, len(info.Deps)) + for _, dep := range info.Deps { + got = append(got, dep.Path+"@"+dep.Version) + } + want := []string{ + "github.com/google/go-cmp@v0.6.0", + "golang.org/x/sync@v0.8.0", + } + if !slices.Equal(got, want) { + t.Fatalf("got deps %v; want %v", got, want) + } +} + +func TestModInfoDataWithoutDeps(t *testing.T) { + info := parseModInfoData(t, modInfoData(nil)) + if len(info.Deps) != 0 { + t.Fatalf("got %d deps; want 0", len(info.Deps)) + } +} + +func TestModInfoDataFormat(t *testing.T) { + got := modInfoData([]moduleInfo{ + {path: "github.com/google/go-cmp", version: "v0.6.0"}, + {path: "golang.org/x/sync", version: "v0.8.0"}, + }) + want := buildInfoStart + + "dep\tgithub.com/google/go-cmp\tv0.6.0\t\n" + + "dep\tgolang.org/x/sync\tv0.8.0\t\n" + + buildInfoEnd + if got != want { + t.Fatalf("got %q; want %q", got, want) + } +} + +func TestShouldEmitBuildInfo(t *testing.T) { + testCases := []struct { + name string + goVersion string + buildmode string + want bool + }{ + {name: "default version", goVersion: "", want: true}, + {name: "go117", goVersion: "1.17", want: false}, + {name: "go117_patch", goVersion: "1.17.1", want: false}, + {name: "go117_rc", goVersion: "1.17rc1", want: false}, + {name: "go118", goVersion: "1.18", want: true}, + {name: "go_prefix", goVersion: "go1.18.3", want: true}, + {name: "plugin", goVersion: "1.24.0", buildmode: "plugin", want: false}, + {name: "unknown", goVersion: "devel go1.26-abcdef", want: true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := shouldEmitBuildInfo(tc.goVersion, tc.buildmode); got != tc.want { + t.Fatalf("shouldEmitBuildInfo(%q, %q) = %t; want %t", tc.goVersion, tc.buildmode, got, tc.want) + } + }) + } +} diff --git a/go/tools/builders/importcfg.go b/go/tools/builders/importcfg.go index c25763a02c..4eef8343ba 100644 --- a/go/tools/builders/importcfg.go +++ b/go/tools/builders/importcfg.go @@ -30,6 +30,7 @@ import ( type archive struct { label, importPath, packagePath, file string importPathAliases []string + modulePath, moduleVersion string } // checkImports verifies that each import in files refers to a @@ -154,7 +155,7 @@ func buildImportcfgFileForCompile(imports map[string]*archive, installSuffix, di return filename, nil } -func buildImportcfgFileForLink(archives []archive, stdPackageListPath, installSuffix, dir string) (string, error) { +func buildImportcfgFileForLink(archives []archive, stdPackageListPath, installSuffix, dir, modinfo string) (string, error) { buf := &bytes.Buffer{} goroot, ok := os.LookupEnv("GOROOT") if !ok { @@ -177,8 +178,15 @@ func buildImportcfgFileForLink(archives []archive, stdPackageListPath, installSu if err := scanner.Err(); err != nil { return "", err } + if modinfo != "" { + fmt.Fprintf(buf, "modinfo %q\n", modinfo) + } depsSeen := map[string]string{} for _, arc := range archives { + label := arc.label + if label == "" { + label = arc.importPath + } if prevLabel, ok := depsSeen[arc.packagePath]; ok { return "", fmt.Errorf(` package conflict error: %s: multiple copies of package passed to linker: @@ -187,13 +195,10 @@ package conflict error: %s: multiple copies of package passed to linker: Set "importmap" to different paths or use 'bazel cquery' to ensure only one package with this path is linked.`, arc.packagePath, - arc.importPath, + label, prevLabel) } - // TODO(zbarsky): The labels are empty, and `importPath` contains the label. - // The parsing is incorrect because arrchiveMultiFlag assuming the formatting from - // `compilepkg.bzl` but `_format_archive` in `link.bzl` formats differently. - depsSeen[arc.packagePath] = arc.importPath + depsSeen[arc.packagePath] = label fmt.Fprintf(buf, "packagefile %s=%s\n", arc.packagePath, arc.file) } f, err := ioutil.TempFile(dir, "importcfg") @@ -270,3 +275,28 @@ func (m *archiveMultiFlag) Set(v string) error { *m = append(*m, a) return nil } + +type linkArchiveMultiFlag []archive + +func (m *linkArchiveMultiFlag) String() string { + if m == nil || len(*m) == 0 { + return "" + } + return fmt.Sprint(*m) +} + +func (m *linkArchiveMultiFlag) Set(v string) error { + parts := strings.Split(v, "=") + if len(parts) != 6 { + return fmt.Errorf("badly formed -arc flag: %s", v) + } + *m = append(*m, archive{ + label: parts[0], + importPath: parts[1], + packagePath: parts[2], + file: abs(parts[3]), + modulePath: parts[4], + moduleVersion: parts[5], + }) + return nil +} diff --git a/go/tools/builders/link.go b/go/tools/builders/link.go index 11dc0abfe1..3445c6791e 100644 --- a/go/tools/builders/link.go +++ b/go/tools/builders/link.go @@ -39,13 +39,14 @@ func link(args []string) error { builderArgs, toolArgs := splitArgs(args) stamps := multiFlag{} xdefs := multiFlag{} - archives := archiveMultiFlag{} + archives := linkArchiveMultiFlag{} flags := flag.NewFlagSet("link", flag.ExitOnError) goenv := envFlags(flags) + goVersion := flags.String("go_version", "", "The SDK Go version from rules_go, without the leading 'go' prefix (for example 1.24.3).") main := flags.String("main", "", "Path to the main archive.") packagePath := flags.String("p", "", "Package path of the main archive.") outFile := flags.String("o", "", "Path to output file.") - flags.Var(&archives, "arc", "Label, package path, and file name of a dependency, separated by '='") + flags.Var(&archives, "arc", "Label, import path, package path, file name, module path, and module version of a dependency, separated by '='") packageList := flags.String("package_list", "", "The file containing the list of standard library packages") buildmode := flags.String("buildmode", "", "Build mode used.") flags.Var(&xdefs, "X", "A string variable to replace in the linked binary (repeated).") @@ -91,7 +92,18 @@ func link(args []string) error { } // Build an importcfg file. - importcfgName, err := buildImportcfgFileForLink(archives, *packageList, goenv.installSuffix, filepath.Dir(*outFile)) + modinfo := "" + if shouldEmitBuildInfo(*goVersion, *buildmode) { + modules := make([]moduleInfo, 0, len(archives)) + for _, arc := range archives { + modules = append(modules, moduleInfo{ + path: arc.modulePath, + version: arc.moduleVersion, + }) + } + modinfo = modInfoData(modules) + } + importcfgName, err := buildImportcfgFileForLink([]archive(archives), *packageList, goenv.installSuffix, filepath.Dir(*outFile), modinfo) if err != nil { return err } diff --git a/tests/bcr/MODULE.bazel b/tests/bcr/MODULE.bazel index 2f0dc4a1b9..1453d5d0f8 100644 --- a/tests/bcr/MODULE.bazel +++ b/tests/bcr/MODULE.bazel @@ -18,7 +18,7 @@ local_path_override( path = "../..", ) -bazel_dep(name = "gazelle", version = "0.36.0") +bazel_dep(name = "gazelle", version = "0.47.0") bazel_dep(name = "platforms", version = "1.0.0") bazel_dep(name = "protobuf", version = "29.0-rc2.bcr.1") bazel_dep(name = "rules_cc", version = "0.2.16") diff --git a/tests/core/go_binary/BUILD.bazel b/tests/core/go_binary/BUILD.bazel index 59048e46e2..b4f4263d72 100644 --- a/tests/core/go_binary/BUILD.bazel +++ b/tests/core/go_binary/BUILD.bazel @@ -23,6 +23,11 @@ go_bazel_test( srcs = ["configurable_attribute_good_test.go"], ) +go_bazel_test( + name = "buildinfo_test", + srcs = ["buildinfo_test.go"], +) + go_binary( name = "hello", srcs = ["hello.go"], diff --git a/tests/core/go_binary/buildinfo_test.go b/tests/core/go_binary/buildinfo_test.go new file mode 100644 index 0000000000..54cfa06164 --- /dev/null +++ b/tests/core/go_binary/buildinfo_test.go @@ -0,0 +1,341 @@ +// Copyright 2026 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 buildinfo_test + +import ( + "encoding/json" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel_testing" +) + +func TestMain(m *testing.M) { + bazel_testing.TestMain(m, bazel_testing.Args{ + Main: ` +-- BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_binary") + +go_binary( + name = "with_dep", + srcs = ["with_dep.go"], + deps = ["@com_github_google_go_cmp//cmp:go_default_library"], +) + +go_binary( + name = "stdlib_only", + srcs = ["stdlib_only.go"], +) + +go_binary( + name = "with_versionless_dep", + srcs = ["with_versionless_dep.go"], + deps = ["@com_example_versionless//:go_default_library"], +) +-- with_dep.go -- +package main + +import ( + "encoding/json" + "os" + "runtime/debug" + + "github.com/google/go-cmp/cmp" +) + +type dep struct { + Path string ` + "`json:\"path\"`" + ` + Version string ` + "`json:\"version\"`" + ` +} + +type output struct { + OK bool ` + "`json:\"ok\"`" + ` + MainPath string ` + "`json:\"main_path\"`" + ` + MainVersion string ` + "`json:\"main_version\"`" + ` + Deps []dep ` + "`json:\"deps\"`" + ` +} + +func main() { + _ = cmp.Equal("same", "same") + + info, ok := debug.ReadBuildInfo() + out := output{OK: ok} + if info != nil { + out.MainPath = info.Main.Path + out.MainVersion = info.Main.Version + for _, module := range info.Deps { + out.Deps = append(out.Deps, dep{Path: module.Path, Version: module.Version}) + } + } + _ = json.NewEncoder(os.Stdout).Encode(out) +} + +-- stdlib_only.go -- +package main + +import ( + "encoding/json" + "os" + "runtime/debug" +) + +type output struct { + OK bool ` + "`json:\"ok\"`" + ` + MainPath string ` + "`json:\"main_path\"`" + ` + MainVersion string ` + "`json:\"main_version\"`" + ` + DepCount int ` + "`json:\"dep_count\"`" + ` +} + +func main() { + info, ok := debug.ReadBuildInfo() + out := output{OK: ok} + if info != nil { + out.MainPath = info.Main.Path + out.MainVersion = info.Main.Version + out.DepCount = len(info.Deps) + } + _ = json.NewEncoder(os.Stdout).Encode(out) +} + +-- with_versionless_dep.go -- +package main + +import ( + "encoding/json" + "os" + "runtime/debug" + + versionless "example.com/versionless" +) + +type dep struct { + Path string ` + "`json:\"path\"`" + ` + Version string ` + "`json:\"version\"`" + ` +} + +type output struct { + OK bool ` + "`json:\"ok\"`" + ` + MainPath string ` + "`json:\"main_path\"`" + ` + MainVersion string ` + "`json:\"main_version\"`" + ` + Deps []dep ` + "`json:\"deps\"`" + ` +} + +func main() { + _ = versionless.Name() + + info, ok := debug.ReadBuildInfo() + out := output{OK: ok} + if info != nil { + out.MainPath = info.Main.Path + out.MainVersion = info.Main.Version + for _, module := range info.Deps { + out.Deps = append(out.Deps, dep{Path: module.Path, Version: module.Version}) + } + } + _ = json.NewEncoder(os.Stdout).Encode(out) +} + +-- deps/com_github_google_go_cmp/MODULE.bazel -- +module(name = "com_github_google_go_cmp") + +bazel_dep(name = "rules_go", repo_name = "io_bazel_rules_go") +bazel_dep(name = "rules_license", version = "1.0.0") + +-- deps/com_github_google_go_cmp/BUILD.bazel -- +load("@rules_license//rules:package_info.bzl", "package_info") + +package_info( + name = "gazelle_generated_package_info", + package_name = "github.com/google/go-cmp", + package_version = "0.6.0", + visibility = ["//:__subpackages__"], +) + +-- deps/com_github_google_go_cmp/cmp/BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "cmp", + srcs = ["cmp.go"], + importpath = "github.com/google/go-cmp/cmp", + applicable_licenses = ["//:gazelle_generated_package_info"], + visibility = ["//visibility:public"], +) + +alias( + name = "go_default_library", + actual = ":cmp", + visibility = ["//visibility:public"], +) + +-- deps/com_github_google_go_cmp/cmp/cmp.go -- +package cmp + +func Equal(x, y string) bool { + return x == y +} + +-- deps/com_example_versionless/MODULE.bazel -- +module(name = "com_example_versionless") + +bazel_dep(name = "rules_go", repo_name = "io_bazel_rules_go") +bazel_dep(name = "rules_license", version = "1.0.0") + +-- deps/com_example_versionless/BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@rules_license//rules:package_info.bzl", "package_info") + +package_info( + name = "gazelle_generated_package_info", + package_name = "example.com/versionless", + package_version = "", + visibility = ["//:__subpackages__"], +) + +go_library( + name = "versionless", + srcs = ["versionless.go"], + importpath = "example.com/versionless", + applicable_licenses = [":gazelle_generated_package_info"], + visibility = ["//visibility:public"], +) + +alias( + name = "go_default_library", + actual = ":versionless", + visibility = ["//visibility:public"], +) + +-- deps/com_example_versionless/versionless.go -- +package versionless + +func Name() string { + return "versionless" +} +`, + ModuleFileSuffix: ` +bazel_dep(name = "rules_license", version = "1.0.0") +bazel_dep(name = "com_github_google_go_cmp") +bazel_dep(name = "com_example_versionless") + +local_path_override( + module_name = "com_github_google_go_cmp", + path = "deps/com_github_google_go_cmp", +) + +local_path_override( + module_name = "com_example_versionless", + path = "deps/com_example_versionless", +) +`, + }) +} + +type dep struct { + Path string `json:"path"` + Version string `json:"version"` +} + +type withDepOutput struct { + OK bool `json:"ok"` + MainPath string `json:"main_path"` + MainVersion string `json:"main_version"` + Deps []dep `json:"deps"` +} + +type stdlibOnlyOutput struct { + OK bool `json:"ok"` + MainPath string `json:"main_path"` + MainVersion string `json:"main_version"` + DepCount int `json:"dep_count"` +} + +func TestReadBuildInfoDeps(t *testing.T) { + stdout, err := bazel_testing.BazelOutput("run", "//:with_dep") + if err != nil { + t.Fatal(err) + } + + var got withDepOutput + if err := json.Unmarshal(stdout, &got); err != nil { + t.Fatalf("unmarshal output %q: %v", stdout, err) + } + if !got.OK { + t.Fatalf("ReadBuildInfo returned ok=false: %+v", got) + } + if got.MainPath != "" || got.MainVersion != "" { + t.Fatalf("got Main %q %q; want empty", got.MainPath, got.MainVersion) + } + if len(got.Deps) == 0 { + t.Fatalf("got no deps: %+v", got) + } + + foundCmp := false + for _, dep := range got.Deps { + if dep.Path == "github.com/google/go-cmp" && dep.Version == "v0.6.0" { + foundCmp = true + break + } + } + if !foundCmp { + t.Fatalf("missing github.com/google/go-cmp@v0.6.0 in %+v", got.Deps) + } +} + +func TestReadBuildInfoWithoutMetadata(t *testing.T) { + stdout, err := bazel_testing.BazelOutput("run", "//:stdlib_only") + if err != nil { + t.Fatal(err) + } + + var got stdlibOnlyOutput + if err := json.Unmarshal(stdout, &got); err != nil { + t.Fatalf("unmarshal output %q: %v", stdout, err) + } + if !got.OK { + t.Fatalf("ReadBuildInfo returned ok=false: %+v", got) + } + if got.MainPath != "" || got.MainVersion != "" { + t.Fatalf("got Main %q %q; want empty", got.MainPath, got.MainVersion) + } + if got.DepCount != 0 { + t.Fatalf("got %d deps; want 0", got.DepCount) + } +} + +func TestReadBuildInfoVersionlessDep(t *testing.T) { + stdout, err := bazel_testing.BazelOutput("run", "//:with_versionless_dep") + if err != nil { + t.Fatal(err) + } + + var got withDepOutput + if err := json.Unmarshal(stdout, &got); err != nil { + t.Fatalf("unmarshal output %q: %v", stdout, err) + } + if !got.OK { + t.Fatalf("ReadBuildInfo returned ok=false: %+v", got) + } + + foundVersionless := false + for _, dep := range got.Deps { + if dep.Path == "example.com/versionless" && dep.Version == "(devel)" { + foundVersionless = true + break + } + } + if !foundVersionless { + t.Fatalf("missing example.com/versionless@(devel) in %+v", got.Deps) + } +} diff --git a/tests/core/starlark/BUILD.bazel b/tests/core/starlark/BUILD.bazel index 3c10ffb7ad..245aebf334 100644 --- a/tests/core/starlark/BUILD.bazel +++ b/tests/core/starlark/BUILD.bazel @@ -1,5 +1,6 @@ load(":common_tests.bzl", "common_test_suite") load(":context_tests.bzl", "context_test_suite") +load(":module_info_tests.bzl", "module_info_test_suite") load(":provider_tests.bzl", "provider_test_suite") load(":sdk_tests.bzl", "sdk_test_suite") @@ -7,6 +8,8 @@ common_test_suite() context_test_suite() +module_info_test_suite() + provider_test_suite() sdk_test_suite() diff --git a/tests/core/starlark/module_info_tests.bzl b/tests/core/starlark/module_info_tests.bzl new file mode 100644 index 0000000000..9caf8b132a --- /dev/null +++ b/tests/core/starlark/module_info_tests.bzl @@ -0,0 +1,127 @@ +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("@rules_license//rules:package_info.bzl", "package_info") +load("//go/private:context.bzl", "module_info_from_metadata") + +ModuleInfoProbeInfo = provider() + +def _module_info_probe_impl(ctx): + info = module_info_from_metadata( + Label(ctx.attr.module_label), + getattr(ctx.attr, "package_metadata", ()), + getattr(ctx.attr, "applicable_licenses", ()), + ) + return [ModuleInfoProbeInfo( + path = info.path, + version = info.version, + )] + +module_info_probe = rule( + implementation = _module_info_probe_impl, + attrs = { + "module_label": attr.string(mandatory = True), + "package_metadata": attr.label_list(), + }, +) + +def _package_metadata_module_info_test_impl(ctx): + env = analysistest.begin(ctx) + info = analysistest.target_under_test(env)[ModuleInfoProbeInfo] + asserts.equals(env, "github.com/google/go-cmp", info.path) + asserts.equals(env, "v0.6.0", info.version) + return analysistest.end(env) + +package_metadata_module_info_test = analysistest.make(_package_metadata_module_info_test_impl) + +def _applicable_licenses_module_info_test_impl(ctx): + env = analysistest.begin(ctx) + info = analysistest.target_under_test(env)[ModuleInfoProbeInfo] + asserts.equals(env, "golang.org/x/sync", info.path) + asserts.equals(env, "v0.8.0", info.version) + return analysistest.end(env) + +applicable_licenses_module_info_test = analysistest.make(_applicable_licenses_module_info_test_impl) + +def _main_workspace_module_info_test_impl(ctx): + env = analysistest.begin(ctx) + info = analysistest.target_under_test(env)[ModuleInfoProbeInfo] + asserts.equals(env, "", info.path) + asserts.equals(env, "", info.version) + return analysistest.end(env) + +main_workspace_module_info_test = analysistest.make(_main_workspace_module_info_test_impl) + +def _versionless_module_info_test_impl(ctx): + env = analysistest.begin(ctx) + info = analysistest.target_under_test(env)[ModuleInfoProbeInfo] + asserts.equals(env, "example.com/versionless", info.path) + asserts.equals(env, "(devel)", info.version) + return analysistest.end(env) + +versionless_module_info_test = analysistest.make(_versionless_module_info_test_impl) + +def module_info_test_suite(): + package_info( + name = "cmp_package_info", + package_name = "github.com/google/go-cmp", + package_version = "0.6.0", + ) + + module_info_probe( + name = "package_metadata_probe", + module_label = "@com_github_google_go_cmp//cmp:go_default_library", + package_metadata = [":cmp_package_info"], + tags = ["manual"], + ) + + package_metadata_module_info_test( + name = "package_metadata_module_info_test", + target_under_test = ":package_metadata_probe", + ) + + package_info( + name = "sync_package_info", + package_name = "golang.org/x/sync", + package_version = "0.8.0", + ) + + module_info_probe( + name = "applicable_licenses_probe", + module_label = "@org_golang_x_sync//errgroup:go_default_library", + applicable_licenses = [":sync_package_info"], + tags = ["manual"], + ) + + applicable_licenses_module_info_test( + name = "applicable_licenses_module_info_test", + target_under_test = ":applicable_licenses_probe", + ) + + module_info_probe( + name = "main_workspace_probe", + module_label = "//tests/core/starlark:main_workspace_probe", + package_metadata = [":cmp_package_info"], + tags = ["manual"], + ) + + main_workspace_module_info_test( + name = "main_workspace_module_info_test", + target_under_test = ":main_workspace_probe", + ) + + package_info( + name = "versionless_package_info", + package_name = "example.com/versionless", + package_version = "", + ) + + module_info_probe( + name = "versionless_probe", + module_label = "@com_github_google_go_cmp//cmp:go_default_library", + package_metadata = [":versionless_package_info"], + tags = ["manual"], + ) + + versionless_module_info_test( + name = "versionless_module_info_test", + target_under_test = ":versionless_probe", + ) diff --git a/tests/integration/reproducibility/reproducibility_test.go b/tests/integration/reproducibility/reproducibility_test.go index 45866afe24..31c5959822 100644 --- a/tests/integration/reproducibility/reproducibility_test.go +++ b/tests/integration/reproducibility/reproducibility_test.go @@ -46,6 +46,7 @@ go_library( go_binary( name = "hello", srcs = ["hello.go"], + deps = ["@com_github_google_go_cmp//cmp:go_default_library"], ) go_binary( @@ -64,12 +65,15 @@ go_binary( -- hello.go -- package main -import "fmt" +import ( + "fmt" + + "github.com/google/go-cmp/cmp" +) func main() { - fmt.Println("hello") + fmt.Println("hello", cmp.Equal("same", "same")) } - -- add.h -- #ifdef __cplusplus extern "C" { @@ -126,6 +130,54 @@ func main() { fmt.Println("In C++, 2 + 2 = ", AddCPP(2, 2)) } +-- deps/com_github_google_go_cmp/MODULE.bazel -- +module(name = "com_github_google_go_cmp") + +bazel_dep(name = "rules_go", repo_name = "io_bazel_rules_go") +bazel_dep(name = "rules_license", version = "1.0.0") + +-- deps/com_github_google_go_cmp/BUILD.bazel -- +load("@rules_license//rules:package_info.bzl", "package_info") + +package_info( + name = "gazelle_generated_package_info", + package_name = "github.com/google/go-cmp", + package_version = "0.6.0", + visibility = ["//:__subpackages__"], +) + +-- deps/com_github_google_go_cmp/cmp/BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "cmp", + srcs = ["cmp.go"], + importpath = "github.com/google/go-cmp/cmp", + applicable_licenses = ["//:gazelle_generated_package_info"], + visibility = ["//visibility:public"], +) + +alias( + name = "go_default_library", + actual = ":cmp", + visibility = ["//visibility:public"], +) + +-- deps/com_github_google_go_cmp/cmp/cmp.go -- +package cmp + +func Equal(x, y string) bool { + return x == y +} + +`, + ModuleFileSuffix: ` +bazel_dep(name = "com_github_google_go_cmp") + +local_path_override( + module_name = "com_github_google_go_cmp", + path = "deps/com_github_google_go_cmp", +) `, }) } diff --git a/third_party/rules_license-stardoc.patch b/third_party/rules_license-stardoc.patch new file mode 100644 index 0000000000..bf5ac974f2 --- /dev/null +++ b/third_party/rules_license-stardoc.patch @@ -0,0 +1,22 @@ +diff -urN a/rules/BUILD b/rules/BUILD +--- a/rules/BUILD 2000-01-01 00:00:00.000000000 -0000 ++++ b/rules/BUILD 2000-01-01 00:00:00.000000000 -0000 +@@ -47,5 +47,5 @@ + exports_files( + glob([ + "*.bzl", + ]), +- visibility = ["//doc_build:__pkg__"], ++ visibility = ["//visibility:public"], + ) +diff -urN a/rules_gathering/BUILD b/rules_gathering/BUILD +--- a/rules_gathering/BUILD 2000-01-01 00:00:00.000000000 -0000 ++++ b/rules_gathering/BUILD 2000-01-01 00:00:00.000000000 -0000 +@@ -29,5 +29,5 @@ + exports_files( + glob([ + "*.bzl", + ]), +- visibility = ["//doc_build:__pkg__"], ++ visibility = ["//visibility:public"], + )