package commands import ( "bytes" "context" "crypto/sha256" "errors" "fmt" "io" "os" "path/filepath" "strings" "time" "github.com/fatih/color" "github.com/gofrs/flock" "github.com/spf13/cobra" "github.com/spf13/pflag" "gopkg.in/yaml.v3" "github.com/golangci/golangci-lint/internal/cache" "github.com/golangci/golangci-lint/internal/pkgcache" "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/fsutils" "github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load" "github.com/golangci/golangci-lint/pkg/goutil" "github.com/golangci/golangci-lint/pkg/lint" "github.com/golangci/golangci-lint/pkg/lint/lintersdb" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/report" "github.com/golangci/golangci-lint/pkg/timeutils" ) type BuildInfo struct { GoVersion string `json:"goVersion"` Version string `json:"version"` Commit string `json:"commit"` Date string `json:"date"` } type Executor struct { rootCmd *cobra.Command runCmd *cobra.Command lintersCmd *cobra.Command exitCode int buildInfo BuildInfo cfg *config.Config // cfg is the unmarshaled data from the golangci config file. log logutils.Log reportData report.Data DBManager *lintersdb.Manager EnabledLintersSet *lintersdb.EnabledSet contextLoader *lint.ContextLoader goenv *goutil.Env fileCache *fsutils.FileCache lineCache *fsutils.LineCache pkgCache *pkgcache.Cache debugf logutils.DebugFunc sw *timeutils.Stopwatch loadGuard *load.Guard flock *flock.Flock } // NewExecutor creates and initializes a new command executor. func NewExecutor(buildInfo BuildInfo) *Executor { startedAt := time.Now() e := &Executor{ cfg: config.NewDefault(), buildInfo: buildInfo, DBManager: lintersdb.NewManager(nil, nil), debugf: logutils.Debug(logutils.DebugKeyExec), } e.debugf("Starting execution...") e.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &e.reportData) // to setup log level early we need to parse config from command line extra time to // find `-v` option commandLineCfg, err := e.getConfigForCommandLine() if err != nil && !errors.Is(err, pflag.ErrHelp) { e.log.Fatalf("Can't get config for command line: %s", err) } if commandLineCfg != nil { logutils.SetupVerboseLog(e.log, commandLineCfg.Run.IsVerbose) switch commandLineCfg.Output.Color { case "always": color.NoColor = false case "never": color.NoColor = true case "auto": // nothing default: e.log.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", commandLineCfg.Output.Color) } } // init of commands must be done before config file reading because // init sets config with the default values of flags e.initRoot() e.initRun() e.initHelp() e.initLinters() e.initConfig() e.initVersion() e.initCache() // init e.cfg by values from config: flags parse will see these values // like the default ones. It will overwrite them only if the same option // is found in command-line: it's ok, command-line has higher priority. r := config.NewFileReader(e.cfg, commandLineCfg, e.log.Child(logutils.DebugKeyConfigReader)) if err = r.Read(); err != nil { e.log.Fatalf("Can't read config: %s", err) } if (commandLineCfg == nil || commandLineCfg.Run.Go == "") && e.cfg != nil && e.cfg.Run.Go == "" { e.cfg.Run.Go = config.DetectGoVersion() } // recreate after getting config e.DBManager = lintersdb.NewManager(e.cfg, e.log) // Slice options must be explicitly set for proper merging of config and command-line options. fixSlicesFlags(e.runCmd.Flags()) fixSlicesFlags(e.lintersCmd.Flags()) e.EnabledLintersSet = lintersdb.NewEnabledSet(e.DBManager, lintersdb.NewValidator(e.DBManager), e.log.Child(logutils.DebugKeyLintersDB), e.cfg) e.goenv = goutil.NewEnv(e.log.Child(logutils.DebugKeyGoEnv)) e.fileCache = fsutils.NewFileCache() e.lineCache = fsutils.NewLineCache(e.fileCache) e.sw = timeutils.NewStopwatch("pkgcache", e.log.Child(logutils.DebugKeyStopwatch)) e.pkgCache, err = pkgcache.NewCache(e.sw, e.log.Child(logutils.DebugKeyPkgCache)) if err != nil { e.log.Fatalf("Failed to build packages cache: %s", err) } e.loadGuard = load.NewGuard() e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child(logutils.DebugKeyLoader), e.goenv, e.lineCache, e.fileCache, e.pkgCache, e.loadGuard) if err = e.initHashSalt(buildInfo.Version); err != nil { e.log.Fatalf("Failed to init hash salt: %s", err) } e.debugf("Initialized executor in %s", time.Since(startedAt)) return e } func (e *Executor) Execute() error { return e.rootCmd.Execute() } func (e *Executor) initHashSalt(version string) error { binSalt, err := computeBinarySalt(version) if err != nil { return fmt.Errorf("failed to calculate binary salt: %w", err) } configSalt, err := computeConfigSalt(e.cfg) if err != nil { return fmt.Errorf("failed to calculate config salt: %w", err) } b := bytes.NewBuffer(binSalt) b.Write(configSalt) cache.SetSalt(b.Bytes()) return nil } func computeBinarySalt(version string) ([]byte, error) { if version != "" && version != "(devel)" { return []byte(version), nil } if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) { return []byte("debug"), nil } p, err := os.Executable() if err != nil { return nil, err } f, err := os.Open(p) if err != nil { return nil, err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return nil, err } return h.Sum(nil), nil } func computeConfigSalt(cfg *config.Config) ([]byte, error) { // We don't hash all config fields to reduce meaningless cache // invalidations. At least, it has a huge impact on tests speed. lintersSettingsBytes, err := yaml.Marshal(cfg.LintersSettings) if err != nil { return nil, fmt.Errorf("failed to json marshal config linter settings: %w", err) } configData := bytes.NewBufferString("linters-settings=") configData.Write(lintersSettingsBytes) configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ",")) h := sha256.New() if _, err := h.Write(configData.Bytes()); err != nil { return nil, err } return h.Sum(nil), nil } func (e *Executor) acquireFileLock() bool { if e.cfg.Run.AllowParallelRunners { e.debugf("Parallel runners are allowed, no locking") return true } lockFile := filepath.Join(os.TempDir(), "golangci-lint.lock") e.debugf("Locking on file %s...", lockFile) f := flock.New(lockFile) const retryDelay = time.Second ctx := context.Background() if !e.cfg.Run.AllowSerialRunners { const totalTimeout = 5 * time.Second var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, totalTimeout) defer cancel() } if ok, _ := f.TryLockContext(ctx, retryDelay); !ok { return false } e.flock = f return true } func (e *Executor) releaseFileLock() { if e.cfg.Run.AllowParallelRunners { return } if err := e.flock.Unlock(); err != nil { e.debugf("Failed to unlock on file: %s", err) } if err := os.Remove(e.flock.Path()); err != nil { e.debugf("Failed to remove lock file: %s", err) } }