From 6aaba7740f88a6c6848e859bef1e54ef7a7ed679 Mon Sep 17 00:00:00 2001 From: Hana Date: Tue, 1 Feb 2022 15:53:46 -0500 Subject: [PATCH] internal/lsp/debug: add go version to gopls version output Add -json flag to output in JSON format. Include the Go version info - go1.18: runtime/debug.BuildInfo.GoVersion - pre go1.18: runtime.Version Restructure ServiceVersion so we embed info from runtime/debug.BuildInfo. Instead of directly using runtime/debug.BuildInfo, we use our own BuildInfo type. That allows: for go1.17 or older versions, we can add GoVersion. for go1.18, we can drop MarshalText that prevents JSON encoding other languages and human can understand (golang/go#51026) For golang/go#49783 Change-Id: Ia5ab50ce1f5e6c3a912654834785ecea7f5034e2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/382274 Trust: Hyang-Ah Hana Kim Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- internal/lsp/cmd/help_test.go | 2 + internal/lsp/cmd/info.go | 10 +++- internal/lsp/cmd/usage/version.hlp | 2 + internal/lsp/debug/buildinfo_go1.12.go | 29 ++++++++++ internal/lsp/debug/buildinfo_go1.18.go | 19 +++++++ internal/lsp/debug/info.go | 77 ++++++++++++-------------- internal/lsp/debug/info_test.go | 47 ++++++++++++++++ 7 files changed, 143 insertions(+), 43 deletions(-) create mode 100644 internal/lsp/debug/buildinfo_go1.12.go create mode 100644 internal/lsp/debug/buildinfo_go1.18.go create mode 100644 internal/lsp/debug/info_test.go diff --git a/internal/lsp/cmd/help_test.go b/internal/lsp/cmd/help_test.go index 1ff1ea28e0..536d19dc21 100644 --- a/internal/lsp/cmd/help_test.go +++ b/internal/lsp/cmd/help_test.go @@ -17,6 +17,8 @@ import ( "golang.org/x/tools/internal/tool" ) +//go:generate go test -run Help -update-help-files + var updateHelpFiles = flag.Bool("update-help-files", false, "Write out the help files instead of checking them") const appName = "gopls" diff --git a/internal/lsp/cmd/info.go b/internal/lsp/cmd/info.go index 2dc8fb57eb..09f453e1a9 100644 --- a/internal/lsp/cmd/info.go +++ b/internal/lsp/cmd/info.go @@ -21,6 +21,8 @@ import ( // version implements the version command. type version struct { + JSON bool `flag:"json" help:"outputs in json format."` + app *Application } @@ -35,8 +37,12 @@ func (v *version) DetailedHelp(f *flag.FlagSet) { // Run prints version information to stdout. func (v *version) Run(ctx context.Context, args ...string) error { - debug.PrintVersionInfo(ctx, os.Stdout, v.app.verbose(), debug.PlainText) - return nil + var mode = debug.PlainText + if v.JSON { + mode = debug.JSON + } + + return debug.PrintVersionInfo(ctx, os.Stdout, v.app.verbose(), mode) } // bug implements the bug command. diff --git a/internal/lsp/cmd/usage/version.hlp b/internal/lsp/cmd/usage/version.hlp index 3ac26b96fe..3a09ddedf6 100644 --- a/internal/lsp/cmd/usage/version.hlp +++ b/internal/lsp/cmd/usage/version.hlp @@ -2,3 +2,5 @@ print the gopls version information Usage: gopls [flags] version + -json + outputs in json format. diff --git a/internal/lsp/debug/buildinfo_go1.12.go b/internal/lsp/debug/buildinfo_go1.12.go new file mode 100644 index 0000000000..2f360dbfc7 --- /dev/null +++ b/internal/lsp/debug/buildinfo_go1.12.go @@ -0,0 +1,29 @@ +// Copyright 2022 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. + +//go:build !go1.18 +// +build !go1.18 + +package debug + +import ( + "runtime" + "runtime/debug" +) + +type BuildInfo struct { + debug.BuildInfo + GoVersion string // Version of Go that produced this binary +} + +func readBuildInfo() (*BuildInfo, bool) { + rinfo, ok := debug.ReadBuildInfo() + if !ok { + return nil, false + } + return &BuildInfo{ + GoVersion: runtime.Version(), + BuildInfo: *rinfo, + }, true +} diff --git a/internal/lsp/debug/buildinfo_go1.18.go b/internal/lsp/debug/buildinfo_go1.18.go new file mode 100644 index 0000000000..4121c4bc9c --- /dev/null +++ b/internal/lsp/debug/buildinfo_go1.18.go @@ -0,0 +1,19 @@ +// Copyright 2022 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. + +//go:build go1.18 +// +build go1.18 + +package debug + +import ( + "runtime/debug" +) + +type BuildInfo debug.BuildInfo + +func readBuildInfo() (*BuildInfo, bool) { + info, ok := debug.ReadBuildInfo() + return (*BuildInfo)(info), ok +} diff --git a/internal/lsp/debug/info.go b/internal/lsp/debug/info.go index 6d4861cff1..bcc2f4f060 100644 --- a/internal/lsp/debug/info.go +++ b/internal/lsp/debug/info.go @@ -7,9 +7,11 @@ package debug import ( "context" + "encoding/json" "fmt" "io" "reflect" + "runtime" "runtime/debug" "sort" "strings" @@ -23,6 +25,7 @@ const ( PlainText = PrintMode(iota) Markdown HTML + JSON ) // Version is a manually-updated mechanism for tracking versions. @@ -31,8 +34,8 @@ const Version = "master" // ServerVersion is the format used by gopls to report its version to the // client. This format is structured so that the client can parse it easily. type ServerVersion struct { - Module - Deps []*Module `json:"deps,omitempty"` + *BuildInfo + Version string } type Module struct { @@ -50,47 +53,24 @@ type ModuleVersion struct { // built in module mode, we return a GOPATH-specific message with the // hardcoded version. func VersionInfo() *ServerVersion { - if info, ok := debug.ReadBuildInfo(); ok { + if info, ok := readBuildInfo(); ok { return getVersion(info) } - path := "gopls, built in GOPATH mode" + buildInfo := &BuildInfo{} + // go1.17 or earlier, part of s.BuildInfo are embedded fields. + buildInfo.Path = "gopls, built in GOPATH mode" + buildInfo.GoVersion = runtime.Version() return &ServerVersion{ - Module: Module{ - ModuleVersion: ModuleVersion{ - Path: path, - Version: Version, - }, - }, + Version: Version, + BuildInfo: buildInfo, } } -func getVersion(info *debug.BuildInfo) *ServerVersion { - serverVersion := ServerVersion{ - Module: Module{ - ModuleVersion: ModuleVersion{ - Path: info.Main.Path, - Version: info.Main.Version, - Sum: info.Main.Sum, - }, - }, +func getVersion(info *BuildInfo) *ServerVersion { + return &ServerVersion{ + Version: Version, + BuildInfo: info, } - for _, d := range info.Deps { - m := &Module{ - ModuleVersion: ModuleVersion{ - Path: d.Path, - Version: d.Version, - Sum: d.Sum, - }, - } - if d.Replace != nil { - m.Replace = &ModuleVersion{ - Path: d.Replace.Path, - Version: d.Replace.Version, - } - } - serverVersion.Deps = append(serverVersion.Deps, m) - } - return &serverVersion } // PrintServerInfo writes HTML debug info to w for the Instance. @@ -111,15 +91,29 @@ func (i *Instance) PrintServerInfo(ctx context.Context, w io.Writer) { // PrintVersionInfo writes version information to w, using the output format // specified by mode. verbose controls whether additional information is // written, including section headers. -func PrintVersionInfo(ctx context.Context, w io.Writer, verbose bool, mode PrintMode) { +func PrintVersionInfo(_ context.Context, w io.Writer, verbose bool, mode PrintMode) error { info := VersionInfo() + if mode == JSON { + return printVersionInfoJSON(w, info) + } + if !verbose { printBuildInfo(w, info, false, mode) - return + return nil } section(w, mode, "Build info", func() { printBuildInfo(w, info, true, mode) }) + return nil +} + +func printVersionInfoJSON(w io.Writer, info *ServerVersion) error { + js, err := json.MarshalIndent(info, "", "\t") + if err != nil { + return err + } + _, err = fmt.Fprint(w, string(js)) + return err } func section(w io.Writer, mode PrintMode, title string, body func()) { @@ -141,16 +135,17 @@ func section(w io.Writer, mode PrintMode, title string, body func()) { func printBuildInfo(w io.Writer, info *ServerVersion, verbose bool, mode PrintMode) { fmt.Fprintf(w, "%v %v\n", info.Path, Version) - printModuleInfo(w, &info.Module, mode) + printModuleInfo(w, info.Main, mode) if !verbose { return } for _, dep := range info.Deps { - printModuleInfo(w, dep, mode) + printModuleInfo(w, *dep, mode) } + fmt.Fprintf(w, "go: %v\n", info.GoVersion) } -func printModuleInfo(w io.Writer, m *Module, mode PrintMode) { +func printModuleInfo(w io.Writer, m debug.Module, _ PrintMode) { fmt.Fprintf(w, " %s@%s", m.Path, m.Version) if m.Sum != "" { fmt.Fprintf(w, " %s", m.Sum) diff --git a/internal/lsp/debug/info_test.go b/internal/lsp/debug/info_test.go new file mode 100644 index 0000000000..5a53628419 --- /dev/null +++ b/internal/lsp/debug/info_test.go @@ -0,0 +1,47 @@ +// Copyright 2022 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 debug exports debug information for gopls. +package debug + +import ( + "bytes" + "context" + "encoding/json" + "runtime" + "testing" +) + +func TestPrintVersionInfoJSON(t *testing.T) { + buf := new(bytes.Buffer) + if err := PrintVersionInfo(context.Background(), buf, true, JSON); err != nil { + t.Fatalf("PrintVersionInfo failed: %v", err) + } + res := buf.Bytes() + + var got ServerVersion + if err := json.Unmarshal(res, &got); err != nil { + t.Fatalf("unexpected output: %v\n%s", err, res) + } + if g, w := got.GoVersion, runtime.Version(); g != w { + t.Errorf("go version = %v, want %v", g, w) + } + if g, w := got.Version, Version; g != w { + t.Errorf("gopls version = %v, want %v", g, w) + } + // Other fields of BuildInfo may not be available during test. +} + +func TestPrintVersionInfoPlainText(t *testing.T) { + buf := new(bytes.Buffer) + if err := PrintVersionInfo(context.Background(), buf, true, PlainText); err != nil { + t.Fatalf("PrintVersionInfo failed: %v", err) + } + res := buf.Bytes() + + // Other fields of BuildInfo may not be available during test. + if !bytes.Contains(res, []byte(Version)) || !bytes.Contains(res, []byte(runtime.Version())) { + t.Errorf("plaintext output = %q,\nwant (version: %v, go: %v)", res, Version, runtime.Version()) + } +}