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 }