220 lines
4.6 KiB
Go

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), "")
}