package internal import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "time" "unicode" "github.com/golangci/golangci-lint/pkg/logutils" ) // Builder runs all the required commands to build a binary. type Builder struct { cfg *Configuration log logutils.Log root string repo string } // NewBuilder creates a new Builder. func NewBuilder(logger logutils.Log, cfg *Configuration, root string) *Builder { return &Builder{ cfg: cfg, log: logger, root: root, repo: filepath.Join(root, "golangci-lint"), } } // Build builds the custom binary. func (b Builder) Build(ctx context.Context) error { b.log.Infof("Cloning golangci-lint repository") err := b.clone(ctx) if err != nil { return fmt.Errorf("clone golangci-lint: %w", err) } b.log.Infof("Adding plugin imports") err = b.updatePluginsFile() if err != nil { return fmt.Errorf("update plugin file: %w", err) } b.log.Infof("Adding replace directives") err = b.addReplaceDirectives(ctx) if err != nil { return fmt.Errorf("add replace directives: %w", err) } b.log.Infof("Running go mod tidy") err = b.goModTidy(ctx) if err != nil { return fmt.Errorf("go mod tidy: %w", err) } b.log.Infof("Building golangci-lint binary") binaryName := b.getBinaryName() err = b.goBuild(ctx, binaryName) if err != nil { return fmt.Errorf("build golangci-lint binary: %w", err) } b.log.Infof("Moving golangci-lint binary") err = b.copyBinary(binaryName) if err != nil { return fmt.Errorf("move golangci-lint binary: %w", err) } return nil } func (b Builder) clone(ctx context.Context) error { //nolint:gosec // the variable is sanitized. cmd := exec.CommandContext(ctx, "git", "clone", "--branch", sanitizeVersion(b.cfg.Version), "--single-branch", "--depth", "1", "-c advice.detachedHead=false", "-q", "https://github.com/golangci/golangci-lint.git", ) cmd.Dir = b.root output, err := cmd.CombinedOutput() if err != nil { b.log.Infof(string(output)) return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err) } return nil } func (b Builder) addReplaceDirectives(ctx context.Context) error { for _, plugin := range b.cfg.Plugins { if plugin.Path == "" { continue } replace := fmt.Sprintf("%s=%s", plugin.Module, plugin.Path) cmd := exec.CommandContext(ctx, "go", "mod", "edit", "-replace", replace) cmd.Dir = b.repo b.log.Infof("run: %s", strings.Join(cmd.Args, " ")) output, err := cmd.CombinedOutput() if err != nil { b.log.Warnf(string(output)) return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err) } } return nil } func (b Builder) goModTidy(ctx context.Context) error { cmd := exec.CommandContext(ctx, "go", "mod", "tidy") cmd.Dir = b.repo output, err := cmd.CombinedOutput() if err != nil { b.log.Warnf(string(output)) return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err) } return nil } func (b Builder) goBuild(ctx context.Context, binaryName string) error { //nolint:gosec // the variable is sanitized. cmd := exec.CommandContext(ctx, "go", "build", "-ldflags", fmt.Sprintf( "-s -w -X 'main.version=%s-custom-gcl' -X 'main.date=%s'", sanitizeVersion(b.cfg.Version), time.Now().UTC().String(), ), "-o", binaryName, "./cmd/golangci-lint", ) cmd.Dir = b.repo output, err := cmd.CombinedOutput() if err != nil { b.log.Warnf(string(output)) return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err) } return nil } func (b Builder) copyBinary(binaryName string) error { src := filepath.Join(b.repo, binaryName) source, err := os.Open(filepath.Clean(src)) if err != nil { return fmt.Errorf("open source file: %w", err) } defer func() { _ = source.Close() }() info, err := source.Stat() if err != nil { return fmt.Errorf("stat source file: %w", err) } if b.cfg.Destination != "" { err = os.MkdirAll(b.cfg.Destination, os.ModePerm) if err != nil { return fmt.Errorf("create destination directory: %w", err) } } dst, err := os.OpenFile(filepath.Join(b.cfg.Destination, binaryName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode()) if err != nil { return fmt.Errorf("create destination file: %w", err) } defer func() { _ = dst.Close() }() _, err = io.Copy(dst, source) if err != nil { return fmt.Errorf("copy source to destination: %w", err) } return nil } func (b Builder) getBinaryName() string { name := b.cfg.Name if runtime.GOOS == "windows" { name += ".exe" } return name } func sanitizeVersion(v string) string { fn := func(c rune) bool { return !(unicode.IsLetter(c) || unicode.IsNumber(c) || c == '.' || c == '/') } return strings.Join(strings.FieldsFunc(v, fn), "") }