]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: make 'go version' apply to files
authorRuss Cox <rsc@golang.org>
Tue, 23 Apr 2019 03:30:03 +0000 (23:30 -0400)
committerRuss Cox <rsc@golang.org>
Fri, 26 Apr 2019 13:52:01 +0000 (13:52 +0000)
Expand "go version" from printing just the version of the
go command itself to being able to print the version of any
go binary (when listed on the command line).

This is a simplified version of rsc.io/goversion, which will
now be deprecated in favor of "go version".

(Preparatory changes to runtime, cmd/go, and cmd/link
made this version information easier to find, allowing a
much simpler implementation than in rsc.io/goversion.)

Fixes #31624.

Change-Id: I5f6f5b8392ce86a617e32ce287f6bf2aceafccff
Reviewed-on: https://go-review.googlesource.com/c/go/+/173343
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
src/cmd/go/alldocs.go
src/cmd/go/internal/version/exe.go [new file with mode: 0644]
src/cmd/go/internal/version/version.go
src/cmd/go/testdata/script/version.txt [new file with mode: 0644]

index 8be5df49b27cf61880f8f7ebc041d1cb38ea422e..aa311a0188d93e0c9979f6fbf2e477fbd4e7ffa6 100644 (file)
 //
 // Usage:
 //
-//     go version
+//     go version [-m] [-v] [file ...]
 //
-// Version prints the Go version, as reported by runtime.Version.
+// Version prints the build information for Go executables.
+//
+// Go version reports the Go version used to build each of the named
+// executable files.
+//
+// If no files are named on the command line, go version prints its own
+// version information.
+//
+// If a directory is named, go version walks that directory, recursively,
+// looking for recognized Go binaries and reporting their versions.
+// By default, go version does not report unrecognized files found
+// during a directory scan. The -v flag causes it to report unrecognized files.
+//
+// The -m flag causes go version to print each executable's embedded
+// module version information, when available. In the output, the modul
+// information consists of multiple lines following the version line, each
+// indented by a leading tab character.
+//
+// See also: go doc runtime/debug.BuildInfo.
 //
 //
 // Report likely mistakes in packages
diff --git a/src/cmd/go/internal/version/exe.go b/src/cmd/go/internal/version/exe.go
new file mode 100644 (file)
index 0000000..99d9ab9
--- /dev/null
@@ -0,0 +1,211 @@
+// Copyright 2019 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 version
+
+import (
+       "bytes"
+       "debug/elf"
+       "debug/macho"
+       "debug/pe"
+       "fmt"
+       "io"
+       "os"
+)
+
+// An exe is a generic interface to an OS executable (ELF, Mach-O, PE).
+type exe interface {
+       // Close closes the underlying file.
+       Close() error
+
+       // ReadData reads and returns up to size byte starting at virtual address addr.
+       ReadData(addr, size uint64) ([]byte, error)
+
+       // DataStart returns the writable data segment start address.
+       DataStart() uint64
+}
+
+// openExe opens file and returns it as an exe.
+func openExe(file string) (exe, error) {
+       f, err := os.Open(file)
+       if err != nil {
+               return nil, err
+       }
+       data := make([]byte, 16)
+       if _, err := io.ReadFull(f, data); err != nil {
+               return nil, err
+       }
+       f.Seek(0, 0)
+       if bytes.HasPrefix(data, []byte("\x7FELF")) {
+               e, err := elf.NewFile(f)
+               if err != nil {
+                       f.Close()
+                       return nil, err
+               }
+               return &elfExe{f, e}, nil
+       }
+       if bytes.HasPrefix(data, []byte("MZ")) {
+               e, err := pe.NewFile(f)
+               if err != nil {
+                       f.Close()
+                       return nil, err
+               }
+               return &peExe{f, e}, nil
+       }
+       if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) {
+               e, err := macho.NewFile(f)
+               if err != nil {
+                       f.Close()
+                       return nil, err
+               }
+               return &machoExe{f, e}, nil
+       }
+       return nil, fmt.Errorf("unrecognized executable format")
+}
+
+// elfExe is the ELF implementation of the exe interface.
+type elfExe struct {
+       os *os.File
+       f  *elf.File
+}
+
+func (x *elfExe) Close() error {
+       return x.os.Close()
+}
+
+func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
+       for _, prog := range x.f.Progs {
+               fmt.Printf("%#x %#x %#x\n", addr, prog.Vaddr, prog.Vaddr+prog.Filesz)
+               if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
+                       n := prog.Vaddr + prog.Filesz - addr
+                       if n > size {
+                               n = size
+                       }
+                       data := make([]byte, n)
+                       _, err := prog.ReadAt(data, int64(addr-prog.Vaddr))
+                       if err != nil {
+                               return nil, err
+                       }
+                       return data, nil
+               }
+       }
+       return nil, fmt.Errorf("address not mapped")
+}
+
+func (x *elfExe) DataStart() uint64 {
+       for _, p := range x.f.Progs {
+               if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W {
+                       return p.Vaddr
+               }
+       }
+       return 0
+}
+
+// peExe is the PE (Windows Portable Executable) implementation of the exe interface.
+type peExe struct {
+       os *os.File
+       f  *pe.File
+}
+
+func (x *peExe) Close() error {
+       return x.os.Close()
+}
+
+func (x *peExe) imageBase() uint64 {
+       switch oh := x.f.OptionalHeader.(type) {
+       case *pe.OptionalHeader32:
+               return uint64(oh.ImageBase)
+       case *pe.OptionalHeader64:
+               return oh.ImageBase
+       }
+       return 0
+}
+
+func (x *peExe) ReadData(addr, size uint64) ([]byte, error) {
+       addr -= x.imageBase()
+       for _, sect := range x.f.Sections {
+               if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) {
+                       n := uint64(sect.VirtualAddress+sect.Size) - addr
+                       if n > size {
+                               n = size
+                       }
+                       data := make([]byte, n)
+                       _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress)))
+                       if err != nil {
+                               return nil, err
+                       }
+                       return data, nil
+               }
+       }
+       return nil, fmt.Errorf("address not mapped")
+}
+
+func (x *peExe) DataStart() uint64 {
+       // Assume data is first writable section.
+       const (
+               IMAGE_SCN_CNT_CODE               = 0x00000020
+               IMAGE_SCN_CNT_INITIALIZED_DATA   = 0x00000040
+               IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080
+               IMAGE_SCN_MEM_EXECUTE            = 0x20000000
+               IMAGE_SCN_MEM_READ               = 0x40000000
+               IMAGE_SCN_MEM_WRITE              = 0x80000000
+               IMAGE_SCN_MEM_DISCARDABLE        = 0x2000000
+               IMAGE_SCN_LNK_NRELOC_OVFL        = 0x1000000
+               IMAGE_SCN_ALIGN_32BYTES          = 0x600000
+       )
+       for _, sect := range x.f.Sections {
+               if sect.VirtualAddress != 0 && sect.Size != 0 &&
+                       sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE {
+                       return uint64(sect.VirtualAddress) + x.imageBase()
+               }
+       }
+       return 0
+}
+
+// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface.
+type machoExe struct {
+       os *os.File
+       f  *macho.File
+}
+
+func (x *machoExe) Close() error {
+       return x.os.Close()
+}
+
+func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
+       for _, load := range x.f.Loads {
+               seg, ok := load.(*macho.Segment)
+               if !ok {
+                       continue
+               }
+               if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 {
+                       if seg.Name == "__PAGEZERO" {
+                               continue
+                       }
+                       n := seg.Addr + seg.Filesz - addr
+                       if n > size {
+                               n = size
+                       }
+                       data := make([]byte, n)
+                       _, err := seg.ReadAt(data, int64(addr-seg.Addr))
+                       if err != nil {
+                               return nil, err
+                       }
+                       return data, nil
+               }
+       }
+       return nil, fmt.Errorf("address not mapped")
+}
+
+func (x *machoExe) DataStart() uint64 {
+       // Assume data is first non-empty writable segment.
+       const RW = 3
+       for _, load := range x.f.Loads {
+               seg, ok := load.(*macho.Segment)
+               if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW {
+                       return seg.Addr
+               }
+       }
+       return 0
+}
index 9344a28dc36af57e2ca5953967a986fe2aad36ea..2b071f6087f0d0f48f230a820896a0767c5c05c6 100644 (file)
 package version
 
 import (
+       "bytes"
+       "encoding/binary"
        "fmt"
+       "os"
+       "path/filepath"
        "runtime"
+       "strings"
 
        "cmd/go/internal/base"
 )
 
 var CmdVersion = &base.Command{
-       Run:       runVersion,
-       UsageLine: "go version",
+       UsageLine: "go version [-m] [-v] [file ...]",
        Short:     "print Go version",
-       Long:      `Version prints the Go version, as reported by runtime.Version.`,
+       Long: `Version prints the build information for Go executables.
+
+Go version reports the Go version used to build each of the named
+executable files.
+
+If no files are named on the command line, go version prints its own
+version information.
+
+If a directory is named, go version walks that directory, recursively,
+looking for recognized Go binaries and reporting their versions.
+By default, go version does not report unrecognized files found
+during a directory scan. The -v flag causes it to report unrecognized files.
+
+The -m flag causes go version to print each executable's embedded
+module version information, when available. In the output, the modul
+information consists of multiple lines following the version line, each
+indented by a leading tab character.
+
+See also: go doc runtime/debug.BuildInfo.
+`,
 }
 
+func init() {
+       CmdVersion.Run = runVersion // break init cycle
+}
+
+var (
+       versionM = CmdVersion.Flag.Bool("m", false, "")
+       versionV = CmdVersion.Flag.Bool("v", false, "")
+)
+
 func runVersion(cmd *base.Command, args []string) {
-       if len(args) != 0 {
-               cmd.Usage()
+       if len(args) == 0 {
+               fmt.Printf("go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
+               return
+       }
+
+       for _, arg := range args {
+               info, err := os.Stat(arg)
+               if err != nil {
+                       fmt.Fprintf(os.Stderr, "%v\n", err)
+                       continue
+               }
+               if info.IsDir() {
+                       scanDir(arg)
+               } else {
+                       scanFile(arg, info, true)
+               }
+       }
+}
+
+// scanDir scans a directory for executables to run scanFile on.
+func scanDir(dir string) {
+       filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+               if info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
+                       scanFile(path, info, *versionV)
+               }
+               return nil
+       })
+}
+
+// isExe reports whether the file should be considered executable.
+func isExe(file string, info os.FileInfo) bool {
+       if runtime.GOOS == "windows" {
+               return strings.HasSuffix(strings.ToLower(file), ".exe")
+       }
+       return info.Mode().IsRegular() && info.Mode()&0111 != 0
+}
+
+// scanFile scans file to try to report the Go and module versions.
+// If mustPrint is true, scanFile will report any error reading file.
+// Otherwise (mustPrint is false, because scanFile is being called
+// by scanDir) scanFile prints nothing for non-Go executables.
+func scanFile(file string, info os.FileInfo, mustPrint bool) {
+       if info.Mode()&os.ModeSymlink != 0 {
+               // Accept file symlinks only.
+               i, err := os.Stat(file)
+               if err != nil || !i.Mode().IsRegular() {
+                       if mustPrint {
+                               fmt.Fprintf(os.Stderr, "%s: symlink\n", file)
+                       }
+                       return
+               }
+               info = i
+       }
+       if !isExe(file, info) {
+               if mustPrint {
+                       fmt.Fprintf(os.Stderr, "%s: not executable file\n", file)
+               }
+               return
        }
 
-       fmt.Printf("go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
+       x, err := openExe(file)
+       if err != nil {
+               if mustPrint {
+                       fmt.Fprintf(os.Stderr, "%s: %v\n", file, err)
+               }
+               return
+       }
+       defer x.Close()
+
+       vers, mod := findVers(x)
+       if vers == "" {
+               if mustPrint {
+                       fmt.Fprintf(os.Stderr, "%s: go version not found\n", file)
+               }
+               return
+       }
+
+       fmt.Printf("%s: %s\n", file, vers)
+       if *versionM && mod != "" {
+               fmt.Printf("\t%s\n", strings.Replace(mod[:len(mod)-1], "\n", "\n\t", -1))
+       }
+}
+
+// The build info blob left by the linker is identified by
+// a 16-byte header, consisting of buildInfoMagic (14 bytes),
+// the binary's pointer size (1 byte),
+// and whether the binary is big endian (1 byte).
+var buildInfoMagic = []byte("\xff Go buildinf:")
+
+// findVers finds and returns the Go version and module version information
+// in the executable x.
+func findVers(x exe) (vers, mod string) {
+       // Read the first 64kB of text to find the build info blob.
+       text := x.DataStart()
+       data, err := x.ReadData(text, 64*1024)
+       if err != nil {
+               return
+       }
+       for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] {
+               if len(data) < 32 {
+                       return
+               }
+       }
+
+       // Decode the blob.
+       ptrSize := int(data[14])
+       bigEndian := data[15] != 0
+       var bo binary.ByteOrder
+       if bigEndian {
+               bo = binary.BigEndian
+       } else {
+               bo = binary.LittleEndian
+       }
+       var readPtr func([]byte) uint64
+       if ptrSize == 4 {
+               readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }
+       } else {
+               readPtr = bo.Uint64
+       }
+       vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))
+       if vers == "" {
+               return
+       }
+       mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
+       if len(mod) >= 33 && mod[len(mod)-17] == '\n' {
+               // Strip module framing.
+               mod = mod[16 : len(mod)-16]
+       } else {
+               mod = ""
+       }
+       return
+}
+
+// readString returns the string at address addr in the executable x.
+func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string {
+       hdr, err := x.ReadData(addr, uint64(2*ptrSize))
+       if err != nil || len(hdr) < 2*ptrSize {
+               return ""
+       }
+       dataAddr := readPtr(hdr)
+       dataLen := readPtr(hdr[ptrSize:])
+       data, err := x.ReadData(dataAddr, dataLen)
+       if err != nil || uint64(len(data)) < dataLen {
+               return ""
+       }
+       return string(data)
 }
diff --git a/src/cmd/go/testdata/script/version.txt b/src/cmd/go/testdata/script/version.txt
new file mode 100644 (file)
index 0000000..84cb23a
--- /dev/null
@@ -0,0 +1,10 @@
+env GO111MODULE=on
+go build -o fortune.exe rsc.io/fortune
+go version fortune.exe
+stdout '^fortune.exe: .+'
+go version -m fortune.exe
+stdout '^\tpath\trsc.io/fortune'
+stdout '^\tmod\trsc.io/fortune\tv1.0.0'
+
+-- go.mod --
+module m