1// Copyright 2011 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 version implements the ``go version'' command.
6package version
7
8import (
9	"bytes"
10	"context"
11	"encoding/binary"
12	"fmt"
13	"io/fs"
14	"os"
15	"path/filepath"
16	"runtime"
17	"strings"
18
19	"cmd/go/internal/base"
20)
21
22var CmdVersion = &base.Command{
23	UsageLine: "go version [-m] [-v] [file ...]",
24	Short:     "print Go version",
25	Long: `Version prints the build information for Go executables.
26
27Go version reports the Go version used to build each of the named
28executable files.
29
30If no files are named on the command line, go version prints its own
31version information.
32
33If a directory is named, go version walks that directory, recursively,
34looking for recognized Go binaries and reporting their versions.
35By default, go version does not report unrecognized files found
36during a directory scan. The -v flag causes it to report unrecognized files.
37
38The -m flag causes go version to print each executable's embedded
39module version information, when available. In the output, the module
40information consists of multiple lines following the version line, each
41indented by a leading tab character.
42
43See also: go doc runtime/debug.BuildInfo.
44`,
45}
46
47func init() {
48	CmdVersion.Run = runVersion // break init cycle
49}
50
51var (
52	versionM = CmdVersion.Flag.Bool("m", false, "")
53	versionV = CmdVersion.Flag.Bool("v", false, "")
54)
55
56func runVersion(ctx context.Context, cmd *base.Command, args []string) {
57	if len(args) == 0 {
58		// If any of this command's flags were passed explicitly, error
59		// out, because they only make sense with arguments.
60		//
61		// Don't error if the flags came from GOFLAGS, since that can be
62		// a reasonable use case. For example, imagine GOFLAGS=-v to
63		// turn "verbose mode" on for all Go commands, which should not
64		// break "go version".
65		if (!base.InGOFLAGS("-m") && *versionM) || (!base.InGOFLAGS("-v") && *versionV) {
66			fmt.Fprintf(os.Stderr, "go version: flags can only be used with arguments\n")
67			base.SetExitStatus(2)
68			return
69		}
70		fmt.Printf("go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
71		return
72	}
73
74	for _, arg := range args {
75		info, err := os.Stat(arg)
76		if err != nil {
77			fmt.Fprintf(os.Stderr, "%v\n", err)
78			base.SetExitStatus(1)
79			continue
80		}
81		if info.IsDir() {
82			scanDir(arg)
83		} else {
84			scanFile(arg, info, true)
85		}
86	}
87}
88
89// scanDir scans a directory for executables to run scanFile on.
90func scanDir(dir string) {
91	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
92		if d.Type().IsRegular() || d.Type()&fs.ModeSymlink != 0 {
93			info, err := d.Info()
94			if err != nil {
95				if *versionV {
96					fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
97				}
98				return nil
99			}
100			scanFile(path, info, *versionV)
101		}
102		return nil
103	})
104}
105
106// isExe reports whether the file should be considered executable.
107func isExe(file string, info fs.FileInfo) bool {
108	if runtime.GOOS == "windows" {
109		return strings.HasSuffix(strings.ToLower(file), ".exe")
110	}
111	return info.Mode().IsRegular() && info.Mode()&0111 != 0
112}
113
114// scanFile scans file to try to report the Go and module versions.
115// If mustPrint is true, scanFile will report any error reading file.
116// Otherwise (mustPrint is false, because scanFile is being called
117// by scanDir) scanFile prints nothing for non-Go executables.
118func scanFile(file string, info fs.FileInfo, mustPrint bool) {
119	if info.Mode()&fs.ModeSymlink != 0 {
120		// Accept file symlinks only.
121		i, err := os.Stat(file)
122		if err != nil || !i.Mode().IsRegular() {
123			if mustPrint {
124				fmt.Fprintf(os.Stderr, "%s: symlink\n", file)
125			}
126			return
127		}
128		info = i
129	}
130
131	if !isExe(file, info) {
132		if mustPrint {
133			fmt.Fprintf(os.Stderr, "%s: not executable file\n", file)
134		}
135		return
136	}
137
138	x, err := openExe(file)
139	if err != nil {
140		if mustPrint {
141			fmt.Fprintf(os.Stderr, "%s: %v\n", file, err)
142		}
143		return
144	}
145	defer x.Close()
146
147	vers, mod := findVers(x)
148	if vers == "" {
149		if mustPrint {
150			fmt.Fprintf(os.Stderr, "%s: go version not found\n", file)
151		}
152		return
153	}
154
155	fmt.Printf("%s: %s\n", file, vers)
156	if *versionM && mod != "" {
157		fmt.Printf("\t%s\n", strings.ReplaceAll(mod[:len(mod)-1], "\n", "\n\t"))
158	}
159}
160
161// The build info blob left by the linker is identified by
162// a 16-byte header, consisting of buildInfoMagic (14 bytes),
163// the binary's pointer size (1 byte),
164// and whether the binary is big endian (1 byte).
165var buildInfoMagic = []byte("\xff Go buildinf:")
166
167// findVers finds and returns the Go version and module version information
168// in the executable x.
169func findVers(x exe) (vers, mod string) {
170	// Read the first 64kB of text to find the build info blob.
171	text := x.DataStart()
172	data, err := x.ReadData(text, 64*1024)
173	if err != nil {
174		return
175	}
176	for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] {
177		if len(data) < 32 {
178			return
179		}
180	}
181
182	// Decode the blob.
183	ptrSize := int(data[14])
184	bigEndian := data[15] != 0
185	var bo binary.ByteOrder
186	if bigEndian {
187		bo = binary.BigEndian
188	} else {
189		bo = binary.LittleEndian
190	}
191	var readPtr func([]byte) uint64
192	if ptrSize == 4 {
193		readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }
194	} else {
195		readPtr = bo.Uint64
196	}
197	vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))
198	if vers == "" {
199		return
200	}
201	mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
202	if len(mod) >= 33 && mod[len(mod)-17] == '\n' {
203		// Strip module framing.
204		mod = mod[16 : len(mod)-16]
205	} else {
206		mod = ""
207	}
208	return
209}
210
211// readString returns the string at address addr in the executable x.
212func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string {
213	hdr, err := x.ReadData(addr, uint64(2*ptrSize))
214	if err != nil || len(hdr) < 2*ptrSize {
215		return ""
216	}
217	dataAddr := readPtr(hdr)
218	dataLen := readPtr(hdr[ptrSize:])
219	data, err := x.ReadData(dataAddr, dataLen)
220	if err != nil || uint64(len(data)) < dataLen {
221		return ""
222	}
223	return string(data)
224}
225