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

     1  // Package ruby provides analysers for Ruby projects.
     2  //
     3  // A `BuildTarget` in Ruby is the directory of the Ruby project, generally
     4  // containing `Gemfile` and `Gemfile.lock`.
     5  package ruby
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"github.com/mitchellh/mapstructure"
    13  
    14  	"github.com/apex/log"
    15  	"github.com/fossas/fossa-cli/buildtools/bundler"
    16  	"github.com/fossas/fossa-cli/exec"
    17  	"github.com/fossas/fossa-cli/graph"
    18  	"github.com/fossas/fossa-cli/module"
    19  	"github.com/fossas/fossa-cli/pkg"
    20  )
    21  
    22  // TODO: add a Ruby sidecar that evaluates `Gemfile` and `*.gemspec`.
    23  
    24  type Analyzer struct {
    25  	RubyCmd     string
    26  	RubyVersion string
    27  
    28  	BundlerCmd     string
    29  	BundlerVersion string
    30  
    31  	Bundler bundler.Bundler
    32  	Module  module.Module
    33  	Options Options
    34  }
    35  
    36  type Options struct {
    37  	Strategy     string `mapstructure:"strategy"`
    38  	LockfilePath string `mapstructure:"gemfile-lock-path"`
    39  }
    40  
    41  func New(m module.Module) (*Analyzer, error) {
    42  	log.WithField("options", m.Options).Debug("constructing analyzer")
    43  
    44  	// Parse and validate options.
    45  	var options Options
    46  	err := mapstructure.Decode(m.Options, &options)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  	log.WithField("options", options).Debug("parsed analyzer options")
    51  
    52  	// Construct analyzer.
    53  	rubyCmd, rubyVersion, err := exec.Which("--version", os.Getenv("FOSSA_RUBY_CMD"), "ruby")
    54  	if err != nil {
    55  		log.Warnf("Could not resolve Ruby")
    56  	}
    57  	bundlerCmd, bundlerVersion, err := exec.Which("--version", os.Getenv("FOSSA_BUNDLER_CMD"), "bundler", "bundle")
    58  	if err != nil {
    59  		log.Warnf("Could not resolve Bundler")
    60  	}
    61  	return &Analyzer{
    62  		RubyCmd:     rubyCmd,
    63  		RubyVersion: rubyVersion,
    64  
    65  		BundlerCmd:     bundlerCmd,
    66  		BundlerVersion: bundlerVersion,
    67  
    68  		Bundler: bundler.Bundler{
    69  			Cmd: bundlerCmd,
    70  		},
    71  		Module:  m,
    72  		Options: options,
    73  	}, nil
    74  }
    75  
    76  // Discover constructs modules in all directories with a `Gemfile`.
    77  func Discover(dir string, options map[string]interface{}) ([]module.Module, error) {
    78  	var modules []module.Module
    79  	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
    80  		if err != nil {
    81  			log.WithError(err).WithField("path", path).Debug("error while walking for discovery")
    82  		}
    83  
    84  		if !info.IsDir() && info.Name() == "Gemfile" {
    85  			moduleName := filepath.Base(path)
    86  
    87  			log.WithFields(log.Fields{
    88  				"path": path,
    89  				"name": moduleName,
    90  			}).Debug("found Ruby module")
    91  			relPath, _ := filepath.Rel(dir, path)
    92  			modules = append(modules, module.Module{
    93  				Name:        moduleName,
    94  				Type:        pkg.Ruby,
    95  				BuildTarget: filepath.Dir(relPath),
    96  				Dir:         filepath.Dir(relPath),
    97  			})
    98  		}
    99  
   100  		return nil
   101  	})
   102  
   103  	if err != nil {
   104  		return nil, fmt.Errorf("Could not find Ruby package manifests: %s", err.Error())
   105  	}
   106  	return modules, nil
   107  }
   108  
   109  // Clean logs a warning and does nothing for Ruby.
   110  func (a *Analyzer) Clean() error {
   111  	// TODO: maybe this should delete `vendor/` for `bundle --deployment`
   112  	// installations? How would we detect that?
   113  	log.Warnf("Clean is not implemented for Ruby")
   114  	return nil
   115  }
   116  
   117  func (a *Analyzer) Build() error {
   118  	return a.Bundler.Install()
   119  }
   120  
   121  func (a *Analyzer) IsBuilt() (bool, error) {
   122  	_, err := a.Bundler.List()
   123  	if err != nil {
   124  		return false, err
   125  	}
   126  	return true, nil
   127  }
   128  
   129  func (a *Analyzer) Analyze() (graph.Deps, error) {
   130  	strategy := "list-lockfile"
   131  
   132  	if a.Options.Strategy != "" {
   133  		strategy = a.Options.Strategy
   134  	}
   135  
   136  	lockfilePath := a.lockfilePath()
   137  
   138  	switch strategy {
   139  	case "list":
   140  		return a.bundlerListAnalyzerStrategy()
   141  	case "lockfile":
   142  		return a.lockfileAnalyzerStrategy(lockfilePath)
   143  	case "list-lockfile":
   144  		fallthrough
   145  	default:
   146  		return a.bundlerListLockfileAnalyzerStrategy(lockfilePath)
   147  	}
   148  }
   149  
   150  func (a *Analyzer) bundlerListLockfileAnalyzerStrategy(lockfilePath string) (graph.Deps, error) {
   151  	lockfile, err := bundler.FromLockfile(lockfilePath)
   152  	if err != nil {
   153  		if a.Options.Strategy != "" {
   154  			return graph.Deps{}, err
   155  		}
   156  
   157  		return a.lockfileAnalyzerStrategy(lockfilePath)
   158  	}
   159  
   160  	gems, err := a.Bundler.List()
   161  	if err == nil {
   162  		imports, deps := FilteredLockfile(gems, lockfile)
   163  
   164  		return graph.Deps{
   165  			Direct:     imports,
   166  			Transitive: deps,
   167  		}, nil
   168  	}
   169  
   170  	if a.Options.Strategy != "" {
   171  		return graph.Deps{}, err
   172  	}
   173  
   174  	deps, err := a.lockfileAnalyzerStrategy(lockfilePath)
   175  
   176  	if err == nil {
   177  		return deps, err
   178  	}
   179  
   180  	if a.Options.Strategy != "" {
   181  		return graph.Deps{}, err
   182  	}
   183  
   184  	return a.bundlerListAnalyzerStrategy()
   185  }
   186  
   187  func (a *Analyzer) bundlerListAnalyzerStrategy() (graph.Deps, error) {
   188  	gems, err := a.Bundler.List()
   189  	if err != nil {
   190  		return graph.Deps{}, err
   191  	}
   192  
   193  	imports, deps := FromGems(gems)
   194  
   195  	return graph.Deps{
   196  		Direct:     imports,
   197  		Transitive: deps,
   198  	}, nil
   199  }
   200  
   201  func (a *Analyzer) lockfileAnalyzerStrategy(lockfilePath string) (graph.Deps, error) {
   202  	lockfile, err := bundler.FromLockfile(lockfilePath)
   203  	if err != nil {
   204  		return graph.Deps{}, err
   205  	}
   206  
   207  	imports, deps := FromLockfile(lockfile)
   208  
   209  	return graph.Deps{
   210  		Direct:     imports,
   211  		Transitive: deps,
   212  	}, nil
   213  }
   214  
   215  func (a *Analyzer) lockfilePath() string {
   216  	if a.Options.LockfilePath != "" {
   217  		return a.Options.LockfilePath
   218  	}
   219  	return filepath.Join(a.Module.Dir, "Gemfile.lock")
   220  }