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