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	"context"
10	"debug/buildinfo"
11	"errors"
12	"fmt"
13	"io/fs"
14	"os"
15	"path/filepath"
16	"runtime"
17	"strings"
18
19	"cmd/go/internal/base"
20	"cmd/go/internal/gover"
21)
22
23var CmdVersion = &base.Command{
24	UsageLine: "go version [-m] [-v] [file ...]",
25	Short:     "print Go version",
26	Long: `Version prints the build information for Go binary files.
27
28Go version reports the Go version used to build each of the named 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 file'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	base.AddChdirFlag(&CmdVersion.Flag)
49	CmdVersion.Run = runVersion // break init cycle
50}
51
52var (
53	versionM = CmdVersion.Flag.Bool("m", false, "")
54	versionV = CmdVersion.Flag.Bool("v", false, "")
55)
56
57func runVersion(ctx context.Context, cmd *base.Command, args []string) {
58	if len(args) == 0 {
59		// If any of this command's flags were passed explicitly, error
60		// out, because they only make sense with arguments.
61		//
62		// Don't error if the flags came from GOFLAGS, since that can be
63		// a reasonable use case. For example, imagine GOFLAGS=-v to
64		// turn "verbose mode" on for all Go commands, which should not
65		// break "go version".
66		var argOnlyFlag string
67		if !base.InGOFLAGS("-m") && *versionM {
68			argOnlyFlag = "-m"
69		} else if !base.InGOFLAGS("-v") && *versionV {
70			argOnlyFlag = "-v"
71		}
72		if argOnlyFlag != "" {
73			fmt.Fprintf(os.Stderr, "go: 'go version' only accepts %s flag with arguments\n", argOnlyFlag)
74			base.SetExitStatus(2)
75			return
76		}
77		v := runtime.Version()
78		if gover.TestVersion != "" {
79			v = gover.TestVersion + " (TESTGO_VERSION)"
80		}
81		fmt.Printf("go version %s %s/%s\n", v, runtime.GOOS, runtime.GOARCH)
82		return
83	}
84
85	for _, arg := range args {
86		info, err := os.Stat(arg)
87		if err != nil {
88			fmt.Fprintf(os.Stderr, "%v\n", err)
89			base.SetExitStatus(1)
90			continue
91		}
92		if info.IsDir() {
93			scanDir(arg)
94		} else {
95			scanFile(arg, info, true)
96		}
97	}
98}
99
100// scanDir scans a directory for binary to run scanFile on.
101func scanDir(dir string) {
102	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
103		if d.Type().IsRegular() || d.Type()&fs.ModeSymlink != 0 {
104			info, err := d.Info()
105			if err != nil {
106				if *versionV {
107					fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
108				}
109				return nil
110			}
111			scanFile(path, info, *versionV)
112		}
113		return nil
114	})
115}
116
117// isGoBinaryCandidate reports whether the file is a candidate to be a Go binary.
118func isGoBinaryCandidate(file string, info fs.FileInfo) bool {
119	if info.Mode().IsRegular() && info.Mode()&0111 != 0 {
120		return true
121	}
122	name := strings.ToLower(file)
123	switch filepath.Ext(name) {
124	case ".so", ".exe", ".dll":
125		return true
126	default:
127		return strings.Contains(name, ".so.")
128	}
129}
130
131// scanFile scans file to try to report the Go and module versions.
132// If mustPrint is true, scanFile will report any error reading file.
133// Otherwise (mustPrint is false, because scanFile is being called
134// by scanDir) scanFile prints nothing for non-Go binaries.
135func scanFile(file string, info fs.FileInfo, mustPrint bool) {
136	if info.Mode()&fs.ModeSymlink != 0 {
137		// Accept file symlinks only.
138		i, err := os.Stat(file)
139		if err != nil || !i.Mode().IsRegular() {
140			if mustPrint {
141				fmt.Fprintf(os.Stderr, "%s: symlink\n", file)
142			}
143			return
144		}
145		info = i
146	}
147
148	bi, err := buildinfo.ReadFile(file)
149	if err != nil {
150		if mustPrint {
151			if pathErr := (*os.PathError)(nil); errors.As(err, &pathErr) && filepath.Clean(pathErr.Path) == filepath.Clean(file) {
152				fmt.Fprintf(os.Stderr, "%v\n", file)
153			} else {
154
155				// Skip errors for non-Go binaries.
156				// buildinfo.ReadFile errors are not fine-grained enough
157				// to know if the file is a Go binary or not,
158				// so try to infer it from the file mode and extension.
159				if isGoBinaryCandidate(file, info) {
160					fmt.Fprintf(os.Stderr, "%s: %v\n", file, err)
161				}
162			}
163		}
164		return
165	}
166
167	fmt.Printf("%s: %s\n", file, bi.GoVersion)
168	bi.GoVersion = "" // suppress printing go version again
169	mod := bi.String()
170	if *versionM && len(mod) > 0 {
171		fmt.Printf("\t%s\n", strings.ReplaceAll(mod[:len(mod)-1], "\n", "\n\t"))
172	}
173}
174