github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/pkg/commands/executor.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/sha256"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/fatih/color"
    14  	"github.com/gofrs/flock"
    15  	"github.com/pkg/errors"
    16  	"github.com/spf13/cobra"
    17  	"github.com/spf13/pflag"
    18  	"gopkg.in/yaml.v3"
    19  
    20  	"github.com/elek/golangci-lint/internal/cache"
    21  	"github.com/elek/golangci-lint/internal/pkgcache"
    22  	"github.com/elek/golangci-lint/pkg/config"
    23  	"github.com/elek/golangci-lint/pkg/fsutils"
    24  	"github.com/elek/golangci-lint/pkg/golinters/goanalysis/load"
    25  	"github.com/elek/golangci-lint/pkg/goutil"
    26  	"github.com/elek/golangci-lint/pkg/lint"
    27  	"github.com/elek/golangci-lint/pkg/lint/lintersdb"
    28  	"github.com/elek/golangci-lint/pkg/logutils"
    29  	"github.com/elek/golangci-lint/pkg/report"
    30  	"github.com/elek/golangci-lint/pkg/timeutils"
    31  )
    32  
    33  type Executor struct {
    34  	rootCmd    *cobra.Command
    35  	runCmd     *cobra.Command
    36  	lintersCmd *cobra.Command
    37  
    38  	exitCode              int
    39  	version, commit, date string
    40  
    41  	cfg               *config.Config
    42  	log               logutils.Log
    43  	reportData        report.Data
    44  	DBManager         *lintersdb.Manager
    45  	EnabledLintersSet *lintersdb.EnabledSet
    46  	contextLoader     *lint.ContextLoader
    47  	goenv             *goutil.Env
    48  	fileCache         *fsutils.FileCache
    49  	lineCache         *fsutils.LineCache
    50  	pkgCache          *pkgcache.Cache
    51  	debugf            logutils.DebugFunc
    52  	sw                *timeutils.Stopwatch
    53  
    54  	loadGuard *load.Guard
    55  	flock     *flock.Flock
    56  }
    57  
    58  func NewExecutor(version, commit, date string) *Executor {
    59  	startedAt := time.Now()
    60  	e := &Executor{
    61  		cfg:       config.NewDefault(),
    62  		version:   version,
    63  		commit:    commit,
    64  		date:      date,
    65  		DBManager: lintersdb.NewManager(nil, nil),
    66  		debugf:    logutils.Debug("exec"),
    67  	}
    68  
    69  	e.debugf("Starting execution...")
    70  	e.log = report.NewLogWrapper(logutils.NewStderrLog(""), &e.reportData)
    71  
    72  	// to setup log level early we need to parse config from command line extra time to
    73  	// find `-v` option
    74  	commandLineCfg, err := e.getConfigForCommandLine()
    75  	if err != nil && err != pflag.ErrHelp {
    76  		e.log.Fatalf("Can't get config for command line: %s", err)
    77  	}
    78  	if commandLineCfg != nil {
    79  		logutils.SetupVerboseLog(e.log, commandLineCfg.Run.IsVerbose)
    80  
    81  		switch commandLineCfg.Output.Color {
    82  		case "always":
    83  			color.NoColor = false
    84  		case "never":
    85  			color.NoColor = true
    86  		case "auto":
    87  			// nothing
    88  		default:
    89  			e.log.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", commandLineCfg.Output.Color)
    90  		}
    91  	}
    92  
    93  	// init of commands must be done before config file reading because
    94  	// init sets config with the default values of flags
    95  	e.initRoot()
    96  	e.initRun()
    97  	e.initHelp()
    98  	e.initLinters()
    99  	e.initConfig()
   100  	e.initVersion()
   101  	e.initCache()
   102  
   103  	// init e.cfg by values from config: flags parse will see these values
   104  	// like the default ones. It will overwrite them only if the same option
   105  	// is found in command-line: it's ok, command-line has higher priority.
   106  
   107  	r := config.NewFileReader(e.cfg, commandLineCfg, e.log.Child("config_reader"))
   108  	if err = r.Read(); err != nil {
   109  		e.log.Fatalf("Can't read config: %s", err)
   110  	}
   111  
   112  	// recreate after getting config
   113  	e.DBManager = lintersdb.NewManager(e.cfg, e.log).WithCustomLinters()
   114  
   115  	e.cfg.LintersSettings.Gocritic.InferEnabledChecks(e.log)
   116  	if err = e.cfg.LintersSettings.Gocritic.Validate(e.log); err != nil {
   117  		e.log.Fatalf("Invalid gocritic settings: %s", err)
   118  	}
   119  
   120  	// Slice options must be explicitly set for proper merging of config and command-line options.
   121  	fixSlicesFlags(e.runCmd.Flags())
   122  	fixSlicesFlags(e.lintersCmd.Flags())
   123  
   124  	e.EnabledLintersSet = lintersdb.NewEnabledSet(e.DBManager,
   125  		lintersdb.NewValidator(e.DBManager), e.log.Child("lintersdb"), e.cfg)
   126  	e.goenv = goutil.NewEnv(e.log.Child("goenv"))
   127  	e.fileCache = fsutils.NewFileCache()
   128  	e.lineCache = fsutils.NewLineCache(e.fileCache)
   129  
   130  	e.sw = timeutils.NewStopwatch("pkgcache", e.log.Child("stopwatch"))
   131  	e.pkgCache, err = pkgcache.NewCache(e.sw, e.log.Child("pkgcache"))
   132  	if err != nil {
   133  		e.log.Fatalf("Failed to build packages cache: %s", err)
   134  	}
   135  	e.loadGuard = load.NewGuard()
   136  	e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child("loader"), e.goenv,
   137  		e.lineCache, e.fileCache, e.pkgCache, e.loadGuard)
   138  	if err = e.initHashSalt(version); err != nil {
   139  		e.log.Fatalf("Failed to init hash salt: %s", err)
   140  	}
   141  	e.debugf("Initialized executor in %s", time.Since(startedAt))
   142  	return e
   143  }
   144  
   145  func (e *Executor) Execute() error {
   146  	return e.rootCmd.Execute()
   147  }
   148  
   149  func (e *Executor) initHashSalt(version string) error {
   150  	binSalt, err := computeBinarySalt(version)
   151  	if err != nil {
   152  		return errors.Wrap(err, "failed to calculate binary salt")
   153  	}
   154  
   155  	configSalt, err := computeConfigSalt(e.cfg)
   156  	if err != nil {
   157  		return errors.Wrap(err, "failed to calculate config salt")
   158  	}
   159  
   160  	var b bytes.Buffer
   161  	b.Write(binSalt)
   162  	b.Write(configSalt)
   163  	cache.SetSalt(b.Bytes())
   164  	return nil
   165  }
   166  
   167  func computeBinarySalt(version string) ([]byte, error) {
   168  	if version != "" && version != "(devel)" {
   169  		return []byte(version), nil
   170  	}
   171  
   172  	if logutils.HaveDebugTag("bin_salt") {
   173  		return []byte("debug"), nil
   174  	}
   175  
   176  	p, err := os.Executable()
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	f, err := os.Open(p)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	defer f.Close()
   185  	h := sha256.New()
   186  	if _, err := io.Copy(h, f); err != nil {
   187  		return nil, err
   188  	}
   189  	return h.Sum(nil), nil
   190  }
   191  
   192  func computeConfigSalt(cfg *config.Config) ([]byte, error) {
   193  	// We don't hash all config fields to reduce meaningless cache
   194  	// invalidations. At least, it has a huge impact on tests speed.
   195  
   196  	lintersSettingsBytes, err := yaml.Marshal(cfg.LintersSettings)
   197  	if err != nil {
   198  		return nil, errors.Wrap(err, "failed to json marshal config linter settings")
   199  	}
   200  
   201  	var configData bytes.Buffer
   202  	configData.WriteString("linters-settings=")
   203  	configData.Write(lintersSettingsBytes)
   204  	configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ","))
   205  
   206  	h := sha256.New()
   207  	if _, err := h.Write(configData.Bytes()); err != nil {
   208  		return nil, err
   209  	}
   210  	return h.Sum(nil), nil
   211  }
   212  
   213  func (e *Executor) acquireFileLock() bool {
   214  	if e.cfg.Run.AllowParallelRunners {
   215  		e.debugf("Parallel runners are allowed, no locking")
   216  		return true
   217  	}
   218  
   219  	lockFile := filepath.Join(os.TempDir(), "golangci-lint.lock")
   220  	e.debugf("Locking on file %s...", lockFile)
   221  	f := flock.New(lockFile)
   222  	const retryDelay = time.Second
   223  
   224  	ctx := context.Background()
   225  	if !e.cfg.Run.AllowSerialRunners {
   226  		const totalTimeout = 5 * time.Second
   227  		var cancel context.CancelFunc
   228  		ctx, cancel = context.WithTimeout(ctx, totalTimeout)
   229  		defer cancel()
   230  	}
   231  	if ok, _ := f.TryLockContext(ctx, retryDelay); !ok {
   232  		return false
   233  	}
   234  
   235  	e.flock = f
   236  	return true
   237  }
   238  
   239  func (e *Executor) releaseFileLock() {
   240  	if e.cfg.Run.AllowParallelRunners {
   241  		return
   242  	}
   243  
   244  	if err := e.flock.Unlock(); err != nil {
   245  		e.debugf("Failed to unlock on file: %s", err)
   246  	}
   247  	if err := os.Remove(e.flock.Path()); err != nil {
   248  		e.debugf("Failed to remove lock file: %s", err)
   249  	}
   250  }
   251  
   252  func (e *Executor) ExitCode() int {
   253  	return e.exitCode
   254  }