mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
357 lines
11 KiB
Go
357 lines
11 KiB
Go
package toolchain
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/github/codeql-go/extractor/util"
|
|
)
|
|
|
|
var V1_14 = util.NewSemVer("v1.14.0")
|
|
var V1_16 = util.NewSemVer("v1.16.0")
|
|
var V1_18 = util.NewSemVer("v1.18.0")
|
|
var V1_21 = util.NewSemVer("v1.21.0")
|
|
|
|
// Check if Go is installed in the environment.
|
|
func IsInstalled() bool {
|
|
_, err := exec.LookPath("go")
|
|
return err == nil
|
|
}
|
|
|
|
// The default Go version that is available on a system and a set of all versions
|
|
// that we know are installed on the system.
|
|
var goVersion = ""
|
|
var goVersions = map[util.SemVer]struct{}{}
|
|
|
|
// Adds an entry to the set of installed Go versions for the normalised `version` number.
|
|
func addGoVersion(version util.SemVer) {
|
|
goVersions[version] = struct{}{}
|
|
}
|
|
|
|
// Returns the current Go version as returned by 'go version', e.g. go1.14.4
|
|
func GetEnvGoVersion() string {
|
|
if goVersion == "" {
|
|
// Since Go 1.21, running 'go version' in a directory with a 'go.mod' file will attempt to
|
|
// download the version of Go specified in there. That may either fail or result in us just
|
|
// being told what's already in 'go.mod'. Setting 'GOTOOLCHAIN' to 'local' will force it
|
|
// to use the local Go toolchain instead.
|
|
cmd := Version()
|
|
|
|
// If 'GOTOOLCHAIN' is already set, then leave it as is. This allows us to force a specific
|
|
// Go version in tests and also allows users to override the system default more generally.
|
|
_, hasToolchainVar := os.LookupEnv("GOTOOLCHAIN")
|
|
if !hasToolchainVar {
|
|
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
|
|
}
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
|
log.Println(string(out))
|
|
log.Fatalf("Unable to run the go command, is it installed?\nError: %s", err.Error())
|
|
}
|
|
|
|
goVersion = parseGoVersion(string(out))
|
|
addGoVersion(util.NewSemVer(goVersion))
|
|
}
|
|
return goVersion
|
|
}
|
|
|
|
// Determines whether, to our knowledge, `version` is available on the current system.
|
|
func HasGoVersion(version util.SemVer) bool {
|
|
_, found := goVersions[version]
|
|
return found
|
|
}
|
|
|
|
// Attempts to install the Go toolchain `version`.
|
|
func InstallVersion(workingDir string, version util.SemVer) bool {
|
|
// No need to install it if we know that it is already installed.
|
|
if HasGoVersion(version) {
|
|
return true
|
|
}
|
|
|
|
// Construct a command to invoke `go version` with `GOTOOLCHAIN=go1.N.0` to give
|
|
// Go a valid toolchain version to download the toolchain we need; subsequent commands
|
|
// should then work even with an invalid version that's still in `go.mod`
|
|
toolchainArg := "GOTOOLCHAIN=go" + version.String()[1:]
|
|
versionCmd := Version()
|
|
versionCmd.Dir = workingDir
|
|
versionCmd.Env = append(os.Environ(), toolchainArg)
|
|
versionCmd.Stdout = os.Stdout
|
|
versionCmd.Stderr = os.Stderr
|
|
|
|
log.Printf(
|
|
"Trying to install Go %s using its canonical representation in `%s`.",
|
|
version,
|
|
workingDir,
|
|
)
|
|
|
|
// Run the command. If something goes wrong, report it to the log and signal failure
|
|
// to the caller.
|
|
if versionErr := versionCmd.Run(); versionErr != nil {
|
|
log.Printf(
|
|
"Failed to invoke `%s go version` in %s: %s\n",
|
|
toolchainArg,
|
|
versionCmd.Dir,
|
|
versionErr.Error(),
|
|
)
|
|
|
|
return false
|
|
}
|
|
|
|
// Add the version to the set of versions that we know are installed and signal
|
|
// success to the caller.
|
|
addGoVersion(version)
|
|
return true
|
|
}
|
|
|
|
// Returns the current Go version in semver format, e.g. v1.14.4
|
|
func GetEnvGoSemVer() util.SemVer {
|
|
goVersion := GetEnvGoVersion()
|
|
if !strings.HasPrefix(goVersion, "go") {
|
|
log.Fatalf("Expected 'go version' output of the form 'go1.2.3'; got '%s'", goVersion)
|
|
}
|
|
return util.NewSemVer(goVersion)
|
|
}
|
|
|
|
// The 'go version' command may output warnings on separate lines before
|
|
// the actual version string is printed. This function parses the output
|
|
// to retrieve just the version string.
|
|
func parseGoVersion(data string) string {
|
|
var lastLine string
|
|
sc := bufio.NewScanner(strings.NewReader(data))
|
|
for sc.Scan() {
|
|
lastLine = sc.Text()
|
|
}
|
|
return strings.Fields(lastLine)[2]
|
|
}
|
|
|
|
// Returns a value indicating whether the system Go toolchain supports workspaces.
|
|
func SupportsWorkspaces() bool {
|
|
return GetEnvGoSemVer().IsAtLeast(V1_18)
|
|
}
|
|
|
|
// Constructs a `*exec.Cmd` for `go` with the specified arguments.
|
|
func GoCommand(arg ...string) *exec.Cmd {
|
|
cmd := exec.Command("go", arg...)
|
|
util.ApplyProxyEnvVars(cmd)
|
|
return cmd
|
|
}
|
|
|
|
// Run `go mod tidy -e` in the directory given by `path`.
|
|
func TidyModule(path string) *exec.Cmd {
|
|
cmd := GoCommand("mod", "tidy", "-e")
|
|
cmd.Dir = path
|
|
return cmd
|
|
}
|
|
|
|
// Run `go mod init` in the directory given by `path`.
|
|
func InitModule(path string) *exec.Cmd {
|
|
moduleName := "codeql/auto-project"
|
|
|
|
if importpath := util.GetImportPath(); importpath != "" {
|
|
// This should be something like `github.com/user/repo`
|
|
moduleName = importpath
|
|
|
|
// If we are not initialising the new module in the root directory of the workspace,
|
|
// append the relative path to the module name.
|
|
if relPath, err := filepath.Rel(".", path); err != nil && relPath != "." {
|
|
moduleName = moduleName + "/" + relPath
|
|
}
|
|
}
|
|
|
|
modInit := GoCommand("mod", "init", moduleName)
|
|
modInit.Dir = path
|
|
return modInit
|
|
}
|
|
|
|
// Constructs a command to run `go mod vendor -e` in the directory given by `path`.
|
|
func VendorModule(path string) *exec.Cmd {
|
|
modVendor := GoCommand("mod", "vendor", "-e")
|
|
modVendor.Dir = path
|
|
return modVendor
|
|
}
|
|
|
|
// Constructs a command to run `go version`.
|
|
func Version() *exec.Cmd {
|
|
version := GoCommand("version")
|
|
return version
|
|
}
|
|
|
|
// Runs `go list` with `format`, `patterns`, and `flags` for the respective inputs.
|
|
func RunList(format string, patterns []string, flags ...string) (string, error) {
|
|
return RunListWithEnv(format, patterns, nil, flags...)
|
|
}
|
|
|
|
// Constructs a `go list` command with `format`, `patterns`, and `flags` for the respective inputs.
|
|
func List(format string, patterns []string, flags ...string) *exec.Cmd {
|
|
return ListWithEnv(format, patterns, nil, flags...)
|
|
}
|
|
|
|
// Runs `go list`.
|
|
func RunListWithEnv(format string, patterns []string, additionalEnv []string, flags ...string) (string, error) {
|
|
cmd := ListWithEnv(format, patterns, additionalEnv, flags...)
|
|
out, err := cmd.Output()
|
|
|
|
if err != nil {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
log.Printf("Warning: go list command failed, output below:\nstdout:\n%s\nstderr:\n%s\n", out, exitErr.Stderr)
|
|
} else {
|
|
log.Printf("Warning: Failed to run go list: %s", err.Error())
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
// Constructs a `go list` command with `format`, `patterns`, and `flags` for the respective inputs
|
|
// and the extra environment variables given by `additionalEnv`.
|
|
func ListWithEnv(format string, patterns []string, additionalEnv []string, flags ...string) *exec.Cmd {
|
|
args := append([]string{"list", "-e", "-f", format}, flags...)
|
|
args = append(args, patterns...)
|
|
cmd := GoCommand(args...)
|
|
cmd.Env = append(os.Environ(), additionalEnv...)
|
|
return cmd
|
|
}
|
|
|
|
// PkgInfo holds package directory and module directory (if any) for a package
|
|
type PkgInfo struct {
|
|
PkgDir string // the directory directly containing source code of this package
|
|
ModDir string // the module directory containing this package, empty if not a module
|
|
}
|
|
|
|
// GetPkgsInfo gets the absolute module and package root directories for the packages matched by the
|
|
// patterns `patterns`. It passes to `go list` the flags specified by `flags`. If `includingDeps`
|
|
// is true, all dependencies will also be included.
|
|
func GetPkgsInfo(patterns []string, includingDeps bool, extractTests bool, flags ...string) (map[string]PkgInfo, error) {
|
|
// enable module mode so that we can find a module root if it exists, even if go module support is
|
|
// disabled by a build
|
|
if includingDeps {
|
|
// the flag `-deps` causes all dependencies to be retrieved
|
|
flags = append(flags, "-deps")
|
|
}
|
|
|
|
if extractTests {
|
|
// Without the `-test` flag, test packages would be omitted from the `go list` output.
|
|
flags = append(flags, "-test")
|
|
}
|
|
|
|
// using -json overrides -f format
|
|
output, err := RunList("", patterns, append(flags, "-json")...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// the output of `go list -json` is a stream of json object
|
|
type goListPkgInfo struct {
|
|
ImportPath string
|
|
Dir string
|
|
Module *struct {
|
|
Dir string
|
|
}
|
|
}
|
|
pkgInfoMapping := make(map[string]PkgInfo)
|
|
streamDecoder := json.NewDecoder(strings.NewReader(output))
|
|
for {
|
|
var pkgInfo goListPkgInfo
|
|
decErr := streamDecoder.Decode(&pkgInfo)
|
|
if decErr == io.EOF {
|
|
break
|
|
}
|
|
if decErr != nil {
|
|
log.Printf("Error decoding output of go list -json: %s", decErr.Error())
|
|
return nil, decErr
|
|
}
|
|
pkgAbsDir, err := filepath.Abs(pkgInfo.Dir)
|
|
if err != nil {
|
|
log.Printf("Unable to make package dir %s absolute: %s", pkgInfo.Dir, err.Error())
|
|
}
|
|
var modAbsDir string
|
|
if pkgInfo.Module != nil {
|
|
modAbsDir, err = filepath.Abs(pkgInfo.Module.Dir)
|
|
if err != nil {
|
|
log.Printf("Unable to make module dir %s absolute: %s", pkgInfo.Module.Dir, err.Error())
|
|
}
|
|
}
|
|
pkgInfoMapping[pkgInfo.ImportPath] = PkgInfo{
|
|
PkgDir: pkgAbsDir,
|
|
ModDir: modAbsDir,
|
|
}
|
|
|
|
if extractTests && strings.Contains(pkgInfo.ImportPath, " [") {
|
|
// Assume " [" is the start of a qualifier, and index the package by its base name
|
|
baseImportPath := strings.Split(pkgInfo.ImportPath, " [")[0]
|
|
pkgInfoMapping[baseImportPath] = pkgInfoMapping[pkgInfo.ImportPath]
|
|
}
|
|
}
|
|
return pkgInfoMapping, nil
|
|
}
|
|
|
|
// GetPkgInfo fills the package info structure for the specified package path.
|
|
// It passes the `go list` the flags specified by `flags`.
|
|
func GetPkgInfo(pkgpath string, flags ...string) PkgInfo {
|
|
return PkgInfo{
|
|
PkgDir: GetPkgDir(pkgpath, flags...),
|
|
ModDir: GetModDir(pkgpath, flags...),
|
|
}
|
|
}
|
|
|
|
// GetModDir gets the absolute directory of the module containing the package with path
|
|
// `pkgpath`. It passes the `go list` the flags specified by `flags`.
|
|
func GetModDir(pkgpath string, flags ...string) string {
|
|
// enable module mode so that we can find a module root if it exists, even if go module support is
|
|
// disabled by a build
|
|
mod, err := RunListWithEnv("{{.Module}}", []string{pkgpath}, []string{"GO111MODULE=on"}, flags...)
|
|
if err != nil || mod == "<nil>" {
|
|
// if the command errors or modules aren't being used, return the empty string
|
|
return ""
|
|
}
|
|
|
|
modDir, err := RunListWithEnv("{{.Module.Dir}}", []string{pkgpath}, []string{"GO111MODULE=on"}, flags...)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
abs, err := filepath.Abs(modDir)
|
|
if err != nil {
|
|
log.Printf("Warning: unable to make %s absolute: %s", modDir, err.Error())
|
|
return ""
|
|
}
|
|
return abs
|
|
}
|
|
|
|
// GetPkgDir gets the absolute directory containing the package with path `pkgpath`. It passes the
|
|
// `go list` command the flags specified by `flags`.
|
|
func GetPkgDir(pkgpath string, flags ...string) string {
|
|
pkgDir, err := RunList("{{.Dir}}", []string{pkgpath}, flags...)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
abs, err := filepath.Abs(pkgDir)
|
|
if err != nil {
|
|
log.Printf("Warning: unable to make %s absolute: %s", pkgDir, err.Error())
|
|
return ""
|
|
}
|
|
return abs
|
|
}
|
|
|
|
// DepErrors checks there are any errors resolving dependencies for `pkgpath`. It passes the `go
|
|
// list` command the flags specified by `flags`.
|
|
func DepErrors(pkgpath string, flags ...string) bool {
|
|
out, err := RunList("{{if .DepsErrors}}error{{else}}{{end}}", []string{pkgpath}, flags...)
|
|
if err != nil {
|
|
// if go list failed, assume dependencies are broken
|
|
return false
|
|
}
|
|
|
|
return out != ""
|
|
}
|