code-intelligence.com/cifuzz@v0.40.0/internal/build/maven/maven.go (about)

     1  package maven
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/pkg/errors"
    13  
    14  	"code-intelligence.com/cifuzz/internal/build"
    15  	"code-intelligence.com/cifuzz/internal/cmdutils"
    16  	"code-intelligence.com/cifuzz/pkg/log"
    17  	"code-intelligence.com/cifuzz/util/fileutil"
    18  )
    19  
    20  type ParallelOptions struct {
    21  	Enabled bool
    22  	NumJobs uint
    23  }
    24  
    25  type BuilderOptions struct {
    26  	ProjectDir string
    27  	Parallel   ParallelOptions
    28  	Stdout     io.Writer
    29  	Stderr     io.Writer
    30  }
    31  
    32  func (opts *BuilderOptions) Validate() error {
    33  	// Check that the project dir is set
    34  	if opts.ProjectDir == "" {
    35  		return errors.New("ProjectDir is not set")
    36  	}
    37  	// Check that the project dir exists and can be accessed
    38  	_, err := os.Stat(opts.ProjectDir)
    39  	if err != nil {
    40  		return errors.WithStack(err)
    41  	}
    42  	return nil
    43  }
    44  
    45  type Builder struct {
    46  	*BuilderOptions
    47  }
    48  
    49  func NewBuilder(opts *BuilderOptions) (*Builder, error) {
    50  	err := opts.Validate()
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	b := &Builder{BuilderOptions: opts}
    56  
    57  	return b, err
    58  }
    59  
    60  func (b *Builder) Build(targetClass string, targetMethod string) (*build.Result, error) {
    61  	var flags []string
    62  	if b.Parallel.Enabled {
    63  		flags = append(flags, "-T")
    64  		if b.Parallel.NumJobs != 0 {
    65  			flags = append(flags, fmt.Sprint(b.Parallel.NumJobs))
    66  		} else {
    67  			// Use one thread per cpu core
    68  			flags = append(flags, "1C")
    69  		}
    70  	}
    71  	args := append(flags, "test-compile")
    72  
    73  	err := b.runMaven(args, b.Stderr, b.Stderr)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	deps, err := b.getExternalDependencies()
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	localDeps, err := b.getLocalDependencies()
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	deps = append(deps, localDeps...)
    89  
    90  	buildDir, err := GetBuildDirectory(b.ProjectDir)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	result := &build.Result{
    95  		Name:         targetClass,
    96  		TargetMethod: targetMethod,
    97  		BuildDir:     buildDir,
    98  		ProjectDir:   b.ProjectDir,
    99  		RuntimeDeps:  deps,
   100  	}
   101  
   102  	return result, nil
   103  }
   104  
   105  func (b *Builder) getExternalDependencies() ([]string, error) {
   106  	tempDir, err := os.MkdirTemp("", "cifuzz-maven-dependencies-*")
   107  	if err != nil {
   108  		return nil, errors.WithStack(err)
   109  	}
   110  	defer fileutil.Cleanup(tempDir)
   111  
   112  	outputPath := filepath.Join(tempDir, "cp")
   113  	outputFlag := "-Dmdep.outputFile=" + outputPath
   114  
   115  	args := []string{
   116  		"dependency:build-classpath",
   117  		outputFlag,
   118  	}
   119  
   120  	err = b.runMaven(args, b.Stderr, b.Stderr)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	output, err := os.ReadFile(outputPath)
   126  	if err != nil {
   127  		return nil, errors.WithStack(err)
   128  	}
   129  
   130  	deps := strings.Split(strings.TrimSpace(string(output)), string(os.PathListSeparator))
   131  	return deps, nil
   132  }
   133  
   134  func (b *Builder) getLocalDependencies() ([]string, error) {
   135  	args := []string{
   136  		"help:evaluate",
   137  		"-Dexpression=project",
   138  		"-DforceStdout",
   139  		"--quiet",
   140  	}
   141  	stdout := new(bytes.Buffer)
   142  	err := b.runMaven(args, stdout, stdout)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	project, err := parseXML(stdout)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	// Append local dependencies which are not listed by "mvn dependency:build-classpath"
   152  	// These directories are configurable
   153  	localDeps := []string{
   154  		project.Build.OutputDirectory,
   155  		project.Build.TestOutputDirectory,
   156  	}
   157  
   158  	return localDeps, nil
   159  }
   160  
   161  func (b *Builder) runMaven(args []string, stdout, stderr io.Writer) error {
   162  	// always run it with the cifuzz profile
   163  	args = append(args, "-Pcifuzz")
   164  	// remove color from output
   165  	args = append(args, "-B")
   166  	cmd := exec.Command(
   167  		"mvn",
   168  		args...,
   169  	)
   170  	// Redirect the command's stdout to stderr to only have
   171  	// reports printed to stdout
   172  	cmd.Stdout = stdout
   173  	cmd.Stderr = stderr
   174  	cmd.Dir = b.ProjectDir
   175  	log.Debugf("Working directory: %s", cmd.Dir)
   176  	log.Debugf("Command: %s", cmd.String())
   177  	err := cmd.Run()
   178  	if err != nil {
   179  		return cmdutils.WrapExecError(errors.WithStack(err), cmd)
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  func GetBuildDirectory(projectDir string) (string, error) {
   186  	cmd := exec.Command("mvn",
   187  		"help:evaluate",
   188  		"-Dexpression=project.build.directory",
   189  		"--quiet",
   190  		"-DforceStdout",
   191  	)
   192  	cmd.Dir = projectDir
   193  	log.Debugf("Working directory: %s", cmd.Dir)
   194  	log.Debugf("Command: %s", cmd.String())
   195  	output, err := cmd.Output()
   196  	if err != nil {
   197  		return "", cmdutils.WrapExecError(errors.WithStack(err), cmd)
   198  	}
   199  
   200  	return string(output), nil
   201  }