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 }