github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/commands/executor.go (about)

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