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

     1  package maven
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/apex/log"
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/fossas/fossa-cli/exec"
    13  	"github.com/fossas/fossa-cli/files"
    14  	"github.com/fossas/fossa-cli/graph"
    15  	"github.com/fossas/fossa-cli/pkg"
    16  )
    17  
    18  type Maven struct {
    19  	Cmd string
    20  }
    21  
    22  func (m *Maven) Clean(dir string) error {
    23  	_, _, err := exec.Run(exec.Cmd{
    24  		Name: m.Cmd,
    25  		Argv: []string{"clean", "--batch-mode"},
    26  		Dir:  dir,
    27  	})
    28  	return err
    29  }
    30  
    31  func (m *Maven) Compile(dir string) error {
    32  	_, _, err := exec.Run(exec.Cmd{
    33  		Name: m.Cmd,
    34  		Argv: []string{"compile", "-DskipTests", "-Drat.skip=true", "--batch-mode"},
    35  		Dir:  dir,
    36  	})
    37  	return err
    38  }
    39  
    40  // A MvnModule can identify a Maven project with the target path.
    41  type MvnModule struct {
    42  	// Name is taken from the module's POM file.
    43  	Name string
    44  
    45  	// Target is the relative path from the root of the FOSSA project to the module's manifest file or to
    46  	// the directory containing the module's "pom.xml". The target may name a file other than "pom.xml" and
    47  	// it may even be the groupId:artifactId string identifying the "Maven project".
    48  	Target string
    49  
    50  	// Dir is the relative path from the root of the FOSSA project to either the module or to a parent module
    51  	// under which this one is listed. This is where `mvn` should be run to get the dependency tree.
    52  	Dir string
    53  }
    54  
    55  // Modules returns a list of all Maven modules at the directory of pomFilePath and below.
    56  func Modules(pomFilePath string, reactorDir string, checked map[string]bool) ([]MvnModule, error) {
    57  	absPath, err := filepath.Abs(pomFilePath)
    58  	if err != nil {
    59  		return nil, errors.Wrapf(err, "could not get absolute path of %q", pomFilePath)
    60  	}
    61  
    62  	if checked[absPath] {
    63  		return nil, nil
    64  	}
    65  
    66  	checked[absPath] = true
    67  
    68  	var pom Manifest
    69  	if err := files.ReadXML(&pom, absPath); err != nil {
    70  		return nil, errors.Wrapf(err, "could not read POM file %q", absPath)
    71  	}
    72  
    73  	modules := make([]MvnModule, 1, 1+len(pom.Modules))
    74  
    75  	name := pom.Name
    76  	if name == "" {
    77  		name = pom.ArtifactID
    78  	}
    79  
    80  	pomRelative, err := filepath.Rel(reactorDir, pomFilePath)
    81  	if err != nil {
    82  		return nil, errors.Wrapf(err, "could not determine relative path of %q from %q", pomFilePath, reactorDir)
    83  	}
    84  
    85  	modules[0] = MvnModule{Name: name, Target: pomRelative, Dir: reactorDir}
    86  
    87  	pomDir := filepath.Dir(pomFilePath)
    88  
    89  	for _, module := range pom.Modules {
    90  		childPath := filepath.Join(pomDir, module)
    91  		childStat, err := os.Stat(childPath)
    92  		if err != nil {
    93  			return nil, errors.Wrapf(err, "could not check type of %q", childPath)
    94  		}
    95  		if childStat.IsDir() {
    96  			// Assume the listed module uses the standard name for the manifest file.
    97  			childPath = filepath.Join(childPath, "pom.xml")
    98  		}
    99  		children, err := Modules(childPath, pomDir, checked)
   100  		if err != nil {
   101  			return nil, errors.Wrapf(err, "could not read child module at %q", childPath)
   102  		}
   103  		modules = append(modules, children...)
   104  	}
   105  
   106  	return modules, nil
   107  }
   108  
   109  // DependencyList runs Maven's dependency:list goal for the specified project.
   110  func (m *Maven) DependencyList(dir, buildTarget string) (string, error) {
   111  	return m.tryDependencyCommands("list", dir, buildTarget)
   112  }
   113  
   114  // DependencyTree runs Maven's dependency:tree goal for the specified project.
   115  func (m *Maven) DependencyTree(dir, buildTarget string) (graph.Deps, error) {
   116  	output, err := m.tryDependencyCommands("tree", dir, buildTarget)
   117  	if err != nil {
   118  		return graph.Deps{}, err
   119  	}
   120  	return ParseDependencyTree(output)
   121  }
   122  
   123  func (m *Maven) tryDependencyCommands(subGoal, dir, buildTarget string) (stdout string, err error) {
   124  	goalArgs := []string{"dependency:" + subGoal, "--batch-mode"}
   125  	cmd := exec.Cmd{Name: m.Cmd, Dir: dir}
   126  
   127  	// First, we try with the buildTarget because we have no idea what it is or whether the reactor already
   128  	// knows about the module.
   129  	cmd.Argv = append(goalArgs, "--projects", buildTarget)
   130  	output, _, err := exec.Run(cmd)
   131  	if err != nil {
   132  		// Now we try to identify the groupId:artifactId identifier for the module and specify the path to
   133  		// the manifest file directly.
   134  		pom, err2 := ResolveManifestFromTarget(buildTarget, dir)
   135  		if err2 != nil {
   136  			// Using buildTarget as a module ID or as a path to a manifest did not work.
   137  			// Return just the error from running the mvn goal the first time.
   138  			return "", errors.Wrap(err, "could not use Maven to list dependencies")
   139  		}
   140  
   141  		// At this point we still don't know if buildTarget is the path to a directory or to a manifest file,
   142  		// so create pomFilePath based on the file extension.
   143  		pomFilePath := buildTarget
   144  		if !strings.HasSuffix(pomFilePath, ".xml") {
   145  			pomFilePath = filepath.Join(pomFilePath, "pom.xml")
   146  		}
   147  
   148  		cmd.Argv = append(goalArgs, "--projects", pom.GroupID+":"+pom.ArtifactID, "--file", pomFilePath)
   149  
   150  		output, _, err2 = exec.Run(cmd)
   151  		err = errors.Wrapf(err2, "could not run %s (original error: %v)", goalArgs[0], err)
   152  	}
   153  	return output, err
   154  }
   155  
   156  //go:generate bash -c "genny -in=$GOPATH/src/github.com/fossas/fossa-cli/graph/readtree.go gen 'Generic=Dependency' | sed -e 's/package graph/package maven/' > readtree_generated.go"
   157  
   158  func ParseDependencyTree(stdin string) (graph.Deps, error) {
   159  	var filteredLines []string
   160  	start := regexp.MustCompile("^\\[INFO\\] --- .*? ---$")
   161  	started := false
   162  	r := regexp.MustCompile("^\\[INFO\\] ([ `+\\\\|-]*)([^ `+\\\\|-].+)$")
   163  	splitReg := regexp.MustCompile("\r?\n")
   164  	for _, line := range splitReg.Split(stdin, -1) {
   165  		if line == "[INFO] " || line == "[INFO] ------------------------------------------------------------------------" {
   166  			started = false
   167  		}
   168  		if strings.HasPrefix(line, "[INFO] Downloading ") {
   169  			continue
   170  		}
   171  		if started {
   172  			filteredLines = append(filteredLines, line)
   173  		}
   174  		if start.MatchString(line) {
   175  			started = true
   176  		}
   177  	}
   178  
   179  	// Remove first line, which is just the direct dependency.
   180  	if len(filteredLines) == 0 {
   181  		return graph.Deps{}, errors.New("error parsing lines")
   182  	}
   183  	filteredLines = filteredLines[1:]
   184  
   185  	depRegex := regexp.MustCompile("([^:]+):([^:]+):([^:]*):([^:]+)")
   186  	imports, deps, err := ReadDependencyTree(filteredLines, func(line string) (int, Dependency, error) {
   187  		log.WithField("line", line).Debug("parsing output line")
   188  		matches := r.FindStringSubmatch(line)
   189  		depth := len(matches[1])
   190  		if depth%3 != 0 {
   191  			// Sanity check
   192  			log.WithField("depth", depth).Fatal("bad depth")
   193  		}
   194  		level := depth / 3
   195  		depMatches := depRegex.FindStringSubmatch(matches[2])
   196  		revision := depMatches[4]
   197  		failed := false
   198  		if strings.HasSuffix(revision, " FAILED") {
   199  			revision = strings.TrimSuffix(revision, " FAILED")
   200  			failed = true
   201  		}
   202  
   203  		dep := Dependency{GroupId: depMatches[1], ArtifactId: depMatches[2], Version: revision, Failed: failed}
   204  		return level, dep, nil
   205  	})
   206  	if err != nil {
   207  		return graph.Deps{}, err
   208  	}
   209  	return graph.Deps{
   210  		Direct:     depsListToImports(imports),
   211  		Transitive: depsMapToPkgGraph(deps),
   212  	}, nil
   213  }
   214  
   215  func depsListToImports(d []Dependency) []pkg.Import {
   216  	i := make([]pkg.Import, 0, len(d))
   217  	for _, dep := range d {
   218  		i = append(i, pkg.Import{
   219  			Resolved: pkg.ID{
   220  				Type:     pkg.Maven,
   221  				Name:     dep.ID(),
   222  				Revision: dep.Version,
   223  			},
   224  		})
   225  	}
   226  	return i
   227  }
   228  
   229  func depsMapToPkgGraph(deps map[Dependency][]Dependency) map[pkg.ID]pkg.Package {
   230  	pkgGraph := make(map[pkg.ID]pkg.Package)
   231  	for parent, children := range deps {
   232  		pack := pkg.Package{
   233  			ID: pkg.ID{
   234  				Type:     pkg.Maven,
   235  				Name:     parent.ID(),
   236  				Revision: parent.Version,
   237  			},
   238  			Imports: make([]pkg.Import, 0, len(children)),
   239  		}
   240  		for _, child := range children {
   241  			pack.Imports = append(pack.Imports, pkg.Import{
   242  				Target: child.Version,
   243  				Resolved: pkg.ID{
   244  					Type:     pkg.Maven,
   245  					Name:     child.ID(),
   246  					Revision: child.Version,
   247  				},
   248  			})
   249  		}
   250  		pkgGraph[pack.ID] = pack
   251  	}
   252  	return pkgGraph
   253  }