1// Copyright 2016 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package bug implements the “go bug” command.
6package bug
7
8import (
9	"bytes"
10	"context"
11	"fmt"
12	"io"
13	urlpkg "net/url"
14	"os"
15	"os/exec"
16	"path/filepath"
17	"regexp"
18	"runtime"
19	"strings"
20
21	"cmd/go/internal/base"
22	"cmd/go/internal/cfg"
23	"cmd/go/internal/envcmd"
24	"cmd/go/internal/web"
25	"cmd/go/internal/work"
26)
27
28var CmdBug = &base.Command{
29	Run:       runBug,
30	UsageLine: "go bug",
31	Short:     "start a bug report",
32	Long: `
33Bug opens the default browser and starts a new bug report.
34The report includes useful system information.
35	`,
36}
37
38func init() {
39	CmdBug.Flag.BoolVar(&cfg.BuildV, "v", false, "")
40	base.AddChdirFlag(&CmdBug.Flag)
41}
42
43func runBug(ctx context.Context, cmd *base.Command, args []string) {
44	if len(args) > 0 {
45		base.Fatalf("go: bug takes no arguments")
46	}
47	work.BuildInit()
48
49	var buf strings.Builder
50	buf.WriteString(bugHeader)
51	printGoVersion(&buf)
52	buf.WriteString("### Does this issue reproduce with the latest release?\n\n\n")
53	printEnvDetails(&buf)
54	buf.WriteString(bugFooter)
55
56	body := buf.String()
57	url := "https://github.com/golang/go/issues/new?body=" + urlpkg.QueryEscape(body)
58	if !web.OpenBrowser(url) {
59		fmt.Print("Please file a new issue at golang.org/issue/new using this template:\n\n")
60		fmt.Print(body)
61	}
62}
63
64const bugHeader = `<!-- Please answer these questions before submitting your issue. Thanks! -->
65
66`
67const bugFooter = `### What did you do?
68
69<!--
70If possible, provide a recipe for reproducing the error.
71A complete runnable program is good.
72A link on play.golang.org is best.
73-->
74
75
76
77### What did you expect to see?
78
79
80
81### What did you see instead?
82
83`
84
85func printGoVersion(w io.Writer) {
86	fmt.Fprintf(w, "### What version of Go are you using (`go version`)?\n\n")
87	fmt.Fprintf(w, "<pre>\n")
88	fmt.Fprintf(w, "$ go version\n")
89	fmt.Fprintf(w, "go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
90	fmt.Fprintf(w, "</pre>\n")
91	fmt.Fprintf(w, "\n")
92}
93
94func printEnvDetails(w io.Writer) {
95	fmt.Fprintf(w, "### What operating system and processor architecture are you using (`go env`)?\n\n")
96	fmt.Fprintf(w, "<details><summary><code>go env</code> Output</summary><br><pre>\n")
97	fmt.Fprintf(w, "$ go env\n")
98	printGoEnv(w)
99	printGoDetails(w)
100	printOSDetails(w)
101	printCDetails(w)
102	fmt.Fprintf(w, "</pre></details>\n\n")
103}
104
105func printGoEnv(w io.Writer) {
106	env := envcmd.MkEnv()
107	env = append(env, envcmd.ExtraEnvVars()...)
108	env = append(env, envcmd.ExtraEnvVarsCostly()...)
109	envcmd.PrintEnv(w, env, false)
110}
111
112func printGoDetails(w io.Writer) {
113	gocmd := filepath.Join(runtime.GOROOT(), "bin/go")
114	printCmdOut(w, "GOROOT/bin/go version: ", gocmd, "version")
115	printCmdOut(w, "GOROOT/bin/go tool compile -V: ", gocmd, "tool", "compile", "-V")
116}
117
118func printOSDetails(w io.Writer) {
119	switch runtime.GOOS {
120	case "darwin", "ios":
121		printCmdOut(w, "uname -v: ", "uname", "-v")
122		printCmdOut(w, "", "sw_vers")
123	case "linux":
124		printCmdOut(w, "uname -sr: ", "uname", "-sr")
125		printCmdOut(w, "", "lsb_release", "-a")
126		printGlibcVersion(w)
127	case "openbsd", "netbsd", "freebsd", "dragonfly":
128		printCmdOut(w, "uname -v: ", "uname", "-v")
129	case "illumos", "solaris":
130		// Be sure to use the OS-supplied uname, in "/usr/bin":
131		printCmdOut(w, "uname -srv: ", "/usr/bin/uname", "-srv")
132		out, err := os.ReadFile("/etc/release")
133		if err == nil {
134			fmt.Fprintf(w, "/etc/release: %s\n", out)
135		} else {
136			if cfg.BuildV {
137				fmt.Printf("failed to read /etc/release: %v\n", err)
138			}
139		}
140	}
141}
142
143func printCDetails(w io.Writer) {
144	printCmdOut(w, "lldb --version: ", "lldb", "--version")
145	cmd := exec.Command("gdb", "--version")
146	out, err := cmd.Output()
147	if err == nil {
148		// There's apparently no combination of command line flags
149		// to get gdb to spit out its version without the license and warranty.
150		// Print up to the first newline.
151		fmt.Fprintf(w, "gdb --version: %s\n", firstLine(out))
152	} else {
153		if cfg.BuildV {
154			fmt.Printf("failed to run gdb --version: %v\n", err)
155		}
156	}
157}
158
159// printCmdOut prints the output of running the given command.
160// It ignores failures; 'go bug' is best effort.
161func printCmdOut(w io.Writer, prefix, path string, args ...string) {
162	cmd := exec.Command(path, args...)
163	out, err := cmd.Output()
164	if err != nil {
165		if cfg.BuildV {
166			fmt.Printf("%s %s: %v\n", path, strings.Join(args, " "), err)
167		}
168		return
169	}
170	fmt.Fprintf(w, "%s%s\n", prefix, bytes.TrimSpace(out))
171}
172
173// firstLine returns the first line of a given byte slice.
174func firstLine(buf []byte) []byte {
175	idx := bytes.IndexByte(buf, '\n')
176	if idx > 0 {
177		buf = buf[:idx]
178	}
179	return bytes.TrimSpace(buf)
180}
181
182// printGlibcVersion prints information about the glibc version.
183// It ignores failures.
184func printGlibcVersion(w io.Writer) {
185	tempdir := os.TempDir()
186	if tempdir == "" {
187		return
188	}
189	src := []byte(`int main() {}`)
190	srcfile := filepath.Join(tempdir, "go-bug.c")
191	outfile := filepath.Join(tempdir, "go-bug")
192	err := os.WriteFile(srcfile, src, 0644)
193	if err != nil {
194		return
195	}
196	defer os.Remove(srcfile)
197	cmd := exec.Command("gcc", "-o", outfile, srcfile)
198	if _, err = cmd.CombinedOutput(); err != nil {
199		return
200	}
201	defer os.Remove(outfile)
202
203	cmd = exec.Command("ldd", outfile)
204	out, err := cmd.CombinedOutput()
205	if err != nil {
206		return
207	}
208	re := regexp.MustCompile(`libc\.so[^ ]* => ([^ ]+)`)
209	m := re.FindStringSubmatch(string(out))
210	if m == nil {
211		return
212	}
213	cmd = exec.Command(m[1])
214	out, err = cmd.Output()
215	if err != nil {
216		return
217	}
218	fmt.Fprintf(w, "%s: %s\n", m[1], firstLine(out))
219
220	// print another line (the one containing version string) in case of musl libc
221	if idx := bytes.IndexByte(out, '\n'); bytes.Contains(out, []byte("musl")) && idx > -1 {
222		fmt.Fprintf(w, "%s\n", firstLine(out[idx+1:]))
223	}
224}
225