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

     1  package composer
     2  
     3  import (
     4  	"encoding/json"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/apex/log"
     9  	"github.com/pkg/errors"
    10  
    11  	"github.com/fossas/fossa-cli/exec"
    12  )
    13  
    14  //go:generate bash -c "genny -in=$GOPATH/src/github.com/fossas/fossa-cli/graph/readtree.go gen 'Generic=Package' | sed -e 's/package graph/package composer/' > readtree_generated.go"
    15  
    16  // A Composer can return the output of the `show` and `install` commands.
    17  type Composer interface {
    18  	// Show returns the output of running the `show` command in dir with optional additional arguments.
    19  	Show(dir string, args ...string) (stdout string, stderr string, err error)
    20  
    21  	// Install returns the output of running the `install` command in dir with optional additional arguments.
    22  	Install(dir string, args ...string) (stdout string, stderr string, err error)
    23  }
    24  
    25  // NewComposer returns a Runner that invokes the real composer binary.
    26  func NewComposer(composerBinary string) Composer {
    27  	return runner(composerBinary)
    28  }
    29  
    30  type Package struct {
    31  	Name        string
    32  	Version     string
    33  	Description string
    34  }
    35  
    36  // A ShowOutput structure has a list of dependencies reported by Composer.
    37  type ShowOutput struct {
    38  	Installed []Package `json:"installed"`
    39  }
    40  
    41  // The runner type implements the Composer interface and execs the composer binary.
    42  type runner string
    43  
    44  func (r runner) Show(dir string, args ...string) (stdout string, stderr string, err error) {
    45  	return exec.Run(exec.Cmd{
    46  		Name: string(r),
    47  		Argv: append([]string{"show"}, args...),
    48  		Dir:  dir,
    49  	})
    50  }
    51  
    52  func (r runner) Install(dir string, args ...string) (stdout string, stderr string, err error) {
    53  	return exec.Run(exec.Cmd{
    54  		Name: string(r),
    55  		Argv: append([]string{"install"}, args...),
    56  		Dir:  dir,
    57  	})
    58  }
    59  
    60  func Dependencies(dir string, c Composer) ([]Package, map[Package][]Package, error) {
    61  	// Run `composer show --format=json --no-ansi` to get resolved versions.
    62  	show, err := Show(dir, c)
    63  	if err != nil {
    64  		return nil, nil, err
    65  	}
    66  
    67  	pkgMap := make(map[string]Package)
    68  	for _, dep := range show.Installed {
    69  		pkgMap[dep.Name] = dep
    70  	}
    71  
    72  	// Run `composer show --tree --no-ansi` to get paths.
    73  	treeOutput, _, err := c.Show(dir, "--tree", "--no-ansi")
    74  	if err != nil {
    75  		return nil, nil, errors.Wrap(err, "could not get dependency list from Composer")
    76  	}
    77  
    78  	// Skip empty lines.
    79  	var filteredLines []string
    80  	for _, line := range strings.Split(treeOutput, "\n") {
    81  		if line != "" {
    82  			filteredLines = append(filteredLines, line)
    83  		}
    84  	}
    85  
    86  	treeLine := regexp.MustCompile("^([ |`-]+)([^ |`-][^ ]+) .*$")
    87  
    88  	imports, deps, err := ReadPackageTree(filteredLines, func(line string) (int, Package, error) {
    89  		if line[0] != '`' && line[0] != '|' && line[0] != ' ' {
    90  			// We're at a top-level package.
    91  			sections := strings.Split(line, " ")
    92  			name := sections[0]
    93  			log.WithField("name", name).Debug("parsing Composer package")
    94  			return 1, pkgMap[name], nil
    95  		}
    96  
    97  		// We're somewhere in the tree.
    98  		matches := treeLine.FindStringSubmatch(line)
    99  		name := matches[2]
   100  		depth := len(matches[1])
   101  		if depth%3 != 0 {
   102  			// Sanity check
   103  			panic(line)
   104  		}
   105  		level := depth/3 + 1
   106  		log.WithFields(log.Fields{
   107  			"name":  name,
   108  			"level": level,
   109  		}).Debug("parsing Composer tree")
   110  
   111  		// Resolve special names.
   112  		if name == "php" || strings.HasPrefix(name, "ext-") {
   113  			return level, Package{Name: name}, nil
   114  		}
   115  
   116  		p, ok := pkgMap[name]
   117  		if !ok {
   118  			log.WithField("name", name).Warn("could not resolve Composer package version")
   119  			return level, Package{Name: name}, nil
   120  		}
   121  		return level, p, nil
   122  	})
   123  
   124  	if err != nil {
   125  		return imports, deps, err
   126  	}
   127  
   128  	// Filter out "comp+php" imports
   129  	filteredImports := make([]Package, 0)
   130  	for _, currPackage := range imports {
   131  		if currPackage.Name != "php" {
   132  			filteredImports = append(filteredImports, currPackage)
   133  		}
   134  	}
   135  
   136  	// Filter out deps, both parent and children with locator as "php"
   137  	filteredDeps := make(map[Package][]Package)
   138  	for parent, children := range deps {
   139  		if parent.Name != "php" {
   140  			_, ok := filteredDeps[parent]
   141  			if !ok {
   142  				filteredDeps[parent] = make([]Package, 0)
   143  			}
   144  			for _, child := range children {
   145  				if child.Name != "php" {
   146  					filteredDeps[parent] = append(filteredDeps[parent], child)
   147  				}
   148  			}
   149  		}
   150  	}
   151  
   152  	return filteredImports, filteredDeps, nil
   153  }
   154  
   155  // Install calls Install on the Composer with dir as the CWD.
   156  func Install(dir string, c Composer) error {
   157  	_, _, err := c.Install(dir, "--prefer-dist", "--no-dev", "--no-plugins", "--no-scripts")
   158  	return err
   159  }
   160  
   161  // Show calls Show on the Composer with dir as the CWD.
   162  func Show(dir string, c Composer) (ShowOutput, error) {
   163  	// Run `composer show --format=json --no-ansi` to get resolved versions
   164  	output, _, err := c.Show(dir, "--format=json", "--no-ansi")
   165  	if err != nil {
   166  		return ShowOutput{}, errors.Wrap(err, "could not get dependency list from Composer")
   167  	}
   168  	var showJSON ShowOutput
   169  	// If there are no deps, `[]` will be returned. Do not attempt to unmarshal it into a Show struct.
   170  	if strings.HasPrefix(output, "[]") {
   171  		return showJSON, nil
   172  	}
   173  	err = json.Unmarshal([]byte(output), &showJSON)
   174  	if err != nil {
   175  		return ShowOutput{}, errors.Wrapf(err, "could not parse dependency list as JSON: %#v", output)
   176  	}
   177  	return showJSON, nil
   178  }