Repository: bazel-contrib/bazel-gazelle

Issue:
FR: generate `pgoprofile` attr to `go_binary` when default profile detected
Golang has supported profile-guided optimizations during compilation since `v1.20`. When using `go build`, the default value of `-pgo` is `auto`, which will cause Go to perform profile-guided optimization if a pprof file named `default.pgo` is co-located in the `main` package directory. See [Go's Profile-Guided Optimization documentation](https://go.dev/doc/pgo) for more details.

Given that `rules_go`'s `go_binary` supports the `pgoprofile` attribute to enable this, `gazelle` should support detecting `default.pgo` within the `main` package, and if it exists, automatically include it as the `go_binary` `pgoprofile` attribute.

In terms of `default.pgo`, this bridges a gap between what would happen by using `go build` directly, and the rules generated by `gazelle`.

Discussion:

Solution patch:
diff --git a/language/go/fileinfo.go b/language/go/fileinfo.go
index 8fb2a9e..c77ed6c 100644
--- a/language/go/fileinfo.go
+++ b/language/go/fileinfo.go
@@ -131,6 +131,10 @@ const (
 
 	// protoExt is applied to .proto files.
 	protoExt
+
+	// pgoExt is applied to .pgo files, expected to be in a pprof format.
+	// Currently, only "default.pgo" is supported. Other *.pgo files are ignored.
+	pgoExt
 )
 
 // fileNameInfo returns information that can be inferred from the name of
@@ -156,6 +160,8 @@ func fileNameInfo(path_ string) fileInfo {
 			ext = csExt
 		case ".proto":
 			ext = protoExt
+		case ".pgo":
+			ext = pgoExt
 		}
 	}
 
diff --git a/language/go/generate.go b/language/go/generate.go
index 8ffb113..97afd1b 100644
--- a/language/go/generate.go
+++ b/language/go/generate.go
@@ -793,6 +793,9 @@ func (g *generator) setCommonAttrs(r *rule.Rule, pkgRel string, visibility []str
 	if target.cgo {
 		r.SetAttr("cgo", true)
 	}
+	if target.pgoprofile != "" {
+		r.SetAttr("pgoprofile", target.pgoprofile)
+	}
 	if !target.clinkopts.isEmpty() {
 		r.SetAttr("clinkopts", g.options(target.clinkopts.build(), pkgRel))
 	}
diff --git a/language/go/kinds.go b/language/go/kinds.go
index 0f465f0..868bffc 100644
--- a/language/go/kinds.go
+++ b/language/go/kinds.go
@@ -17,6 +17,7 @@ package golang
 
 import (
 	"fmt"
+
 	"github.com/bazelbuild/bazel-gazelle/rule"
 )
 
@@ -38,14 +39,15 @@ var goKinds = map[string]rule.KindInfo{
 		},
 		SubstituteAttrs: map[string]bool{"embed": true},
 		MergeableAttrs: map[string]bool{
-			"cgo":       true,
-			"clinkopts": true,
-			"cppopts":   true,
-			"copts":     true,
-			"cxxopts":   true,
-			"embed":     true,
-			"embedsrcs": true,
-			"srcs":      true,
+			"cgo":        true,
+			"clinkopts":  true,
+			"cppopts":    true,
+			"copts":      true,
+			"cxxopts":    true,
+			"embed":      true,
+			"embedsrcs":  true,
+			"pgoprofile": true,
+			"srcs":       true,
 		},
 		ResolveAttrs: map[string]bool{"deps": true},
 	},
diff --git a/language/go/package.go b/language/go/package.go
index dc16067..d9c8e6f 100644
--- a/language/go/package.go
+++ b/language/go/package.go
@@ -46,6 +46,7 @@ type goPackage struct {
 type goTarget struct {
 	sources, embedSrcs, imports, cppopts, copts, cxxopts, clinkopts platformStringsBuilder
 	cgo, hasInternalTest                                            bool
+	pgoprofile                                                      string
 }
 
 // protoTarget contains information used to generate a go_proto_library rule.
@@ -105,6 +106,13 @@ func (pkg *goPackage) addFile(c *config.Config, er *embedResolver, info fileInfo
 			// from information emitted by the proto language extension.
 			pkg.proto.addFile(info)
 		}
+	case info.ext == pgoExt:
+		if info.name == "default.pgo" {
+			// Only use auto-include the *.pgo file if it is "default.pgo", as this file
+			// would be used by `go build` if `-pgo` is not specified.
+			// See https://pkg.go.dev/cmd/go for more details.
+			pkg.binary.pgoprofile = info.name
+		}
 	case info.isTest:
 		if getGoConfig(c).testMode == fileTestMode || len(pkg.tests) == 0 {
 			pkg.tests = append(pkg.tests, goTarget{})



Test patch:
diff --git a/language/go/fileinfo_test.go b/language/go/fileinfo_test.go
index 0bd42b8..5a914df 100644
--- a/language/go/fileinfo_test.go
+++ b/language/go/fileinfo_test.go
@@ -235,6 +235,20 @@ func TestFileNameInfo(t *testing.T) {
 				ext: cExt,
 			},
 		},
+		{
+			"pgo (default) file",
+			"default.pgo",
+			fileInfo{
+				ext: pgoExt,
+			},
+		},
+		{
+			"pgo (other) file",
+			"notDefault.pgo",
+			fileInfo{
+				ext: pgoExt,
+			},
+		},
 		{
 			"ignored test file",
 			"foo_test.py",
diff --git a/language/go/testdata/bin_with_default_pgo/BUILD.want b/language/go/testdata/bin_with_default_pgo/BUILD.want
new file mode 100644
index 0000000..c45d35a
--- /dev/null
+++ b/language/go/testdata/bin_with_default_pgo/BUILD.want
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "bin_with_default_pgo_lib",
+    srcs = ["main.go"],
+    _gazelle_imports = [
+        "example.com/repo/lib",
+        "fmt",
+    ],
+    importpath = "example.com/repo/bin_with_default_pgo",
+    visibility = ["//visibility:private"],
+)
+
+go_binary(
+    name = "bin_with_default_pgo",
+    _gazelle_imports = [],
+    embed = [":bin_with_default_pgo_lib"],
+    pgoprofile = "default.pgo",
+    visibility = ["//visibility:public"],
+)
diff --git a/language/go/testdata/bin_with_default_pgo/default.pgo b/language/go/testdata/bin_with_default_pgo/default.pgo
new file mode 100644
index 0000000..6000cfc
--- /dev/null
+++ b/language/go/testdata/bin_with_default_pgo/default.pgo
@@ -0,0 +1 @@
+Mmmmm, tasty pprof data...
\ No newline at end of file
diff --git a/language/go/testdata/bin/main.go b/language/go/testdata/bin_with_default_pgo/main.go
similarity index 100%
rename from language/go/testdata/bin/main.go
rename to language/go/testdata/bin_with_default_pgo/main.go
diff --git a/language/go/testdata/bin/BUILD.want b/language/go/testdata/bin_with_other_pgo/BUILD.want
similarity index 64%
rename from language/go/testdata/bin/BUILD.want
rename to language/go/testdata/bin_with_other_pgo/BUILD.want
index 51ae877..45126eb 100644
--- a/language/go/testdata/bin/BUILD.want
+++ b/language/go/testdata/bin_with_other_pgo/BUILD.want
@@ -1,19 +1,19 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 
 go_library(
-    name = "bin_lib",
+    name = "bin_with_other_pgo_lib",
     srcs = ["main.go"],
     _gazelle_imports = [
         "example.com/repo/lib",
         "fmt",
     ],
-    importpath = "example.com/repo/bin",
+    importpath = "example.com/repo/bin_with_other_pgo",
     visibility = ["//visibility:private"],
 )
 
 go_binary(
-    name = "bin",
+    name = "bin_with_other_pgo",
     _gazelle_imports = [],
-    embed = [":bin_lib"],
+    embed = [":bin_with_other_pgo_lib"],
     visibility = ["//visibility:public"],
 )
diff --git a/language/go/testdata/bin_with_other_pgo/main.go b/language/go/testdata/bin_with_other_pgo/main.go
new file mode 100644
index 0000000..122188d
--- /dev/null
+++ b/language/go/testdata/bin_with_other_pgo/main.go
@@ -0,0 +1,26 @@
+/* Copyright 2016 The Bazel Authors. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+
+	"example.com/repo/lib"
+)
+
+func main() {
+	fmt.Println(lib.Answer())
+}
diff --git a/language/go/testdata/bin_with_other_pgo/other.pgo b/language/go/testdata/bin_with_other_pgo/other.pgo
new file mode 100644
index 0000000..6000cfc
--- /dev/null
+++ b/language/go/testdata/bin_with_other_pgo/other.pgo
@@ -0,0 +1 @@
+Mmmmm, tasty pprof data...
\ No newline at end of file