github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/buildtools/gomodules/gomodules.go (about)

     1  package gomodules
     2  
     3  import (
     4  	"encoding/json"
     5  	"os"
     6  	"path"
     7  	"strings"
     8  
     9  	"github.com/pkg/errors"
    10  
    11  	"github.com/fossas/fossa-cli/buildtools"
    12  	"github.com/fossas/fossa-cli/exec"
    13  	"github.com/fossas/fossa-cli/pkg"
    14  )
    15  
    16  // Resolver maps modules from import paths to their pkg.Import representation.
    17  type Resolver struct {
    18  	pathMap map[string]pkg.Import
    19  }
    20  
    21  // module is used to unmarshal `go list -m -json all`.
    22  type module struct {
    23  	Path    string
    24  	Version string
    25  	Replace replace
    26  }
    27  
    28  type replace struct {
    29  	Path    string
    30  	Version string
    31  }
    32  
    33  // Resolve calculates a package's module path from its import path,
    34  // and returns the resolved pkg.Import for the module path.
    35  func (r Resolver) Resolve(importpath string) (pkg.Import, error) {
    36  	for p := importpath; p != "." && p != "/"; p = path.Dir(p) {
    37  		revision, ok := r.pathMap[p]
    38  		if ok {
    39  			return revision, nil
    40  		}
    41  	}
    42  	return pkg.Import{}, buildtools.ErrNoRevisionForPackage
    43  }
    44  
    45  // New connects goModuleList and a parser to return a golang.Resolver.
    46  func New(dir string) (Resolver, error) {
    47  	moduleJSON, err := goModuleList(dir)
    48  	if err != nil {
    49  		return Resolver{}, errors.Wrap(err, "Could not run go list")
    50  	}
    51  
    52  	resolver, err := parseModuleJSON(moduleJSON)
    53  	if err != nil {
    54  		return Resolver{}, errors.Wrap(err, "Could not parse json")
    55  	}
    56  	return resolver, nil
    57  }
    58  
    59  // Mock creates a golang.Resolver using any string input.
    60  func Mock(modules string) (Resolver, error) {
    61  	return parseModuleJSON(modules)
    62  }
    63  
    64  // parseModuleJSON returns a golang.Resolver from the output of `go list -m -json all`.
    65  // Replaced modules are handled in place and added to the pathMap.
    66  func parseModuleJSON(moduleJSON string) (Resolver, error) {
    67  	resolver := Resolver{}
    68  	var modList []module
    69  	// The output for each module is valid JSON, but the output overall is not.
    70  	err := json.Unmarshal([]byte("["+strings.Replace(moduleJSON, "}\n{", "},{", -1)+"]"), &modList)
    71  	if err != nil {
    72  		return resolver, errors.Wrap(err, "Could not unmarshal JSON into module list")
    73  	}
    74  
    75  	normalizedModules := make(map[string]pkg.Import)
    76  	for _, mod := range modList {
    77  		importpath := mod.Path
    78  
    79  		// Handle replaced modules.
    80  		emptyReplace := replace{}
    81  		if mod.Replace != emptyReplace {
    82  			mod = module{
    83  				Version: mod.Replace.Version,
    84  				Path:    mod.Replace.Path,
    85  			}
    86  		}
    87  
    88  		version := extractRevision(mod.Version)
    89  		normalizedModules[importpath] = pkg.Import{
    90  			Target: version,
    91  			Resolved: pkg.ID{
    92  				Type:     pkg.Go,
    93  				Name:     mod.Path,
    94  				Revision: version,
    95  			},
    96  		}
    97  	}
    98  
    99  	resolver.pathMap = normalizedModules
   100  	return resolver, nil
   101  }
   102  
   103  func goModuleList(path string) (string, error) {
   104  	cmd, _, err := exec.Which("version", os.Getenv("FOSSA_GO_CMD"), "go")
   105  	if err != nil {
   106  		return "", err
   107  	}
   108  	stdout, stderr, err := exec.Run(exec.Cmd{
   109  		Name: cmd,
   110  		Argv: []string{"list", "-m", "-json", "all"},
   111  		Dir:  path,
   112  	})
   113  	if err != nil {
   114  		return "", errors.Errorf("Could not run `go list -m -json all` within path %s: %s (%s)", path, strings.TrimSpace(stderr), err)
   115  	}
   116  
   117  	return stdout, nil
   118  }
   119  
   120  // extractRevision returns the correct revision from a gomodules version which has
   121  // the form of "version-date-gitSHA" or "version". If gitSHA is present we want to
   122  // return it as this means version is v0.0.0.
   123  func extractRevision(version string) string {
   124  	split := strings.Split(version, "-")
   125  	if len(split) < 3 {
   126  		return strings.TrimSuffix(version, "+incompatible")
   127  	}
   128  	return split[2]
   129  }