github.com/triarius/goreleaser@v1.12.5/internal/builders/golang/build.go (about)

     1  package golang
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/parser"
     7  	"go/token"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/caarlos0/log"
    16  	"github.com/triarius/goreleaser/internal/artifact"
    17  	"github.com/triarius/goreleaser/internal/builders/buildtarget"
    18  	"github.com/triarius/goreleaser/internal/tmpl"
    19  	api "github.com/triarius/goreleaser/pkg/build"
    20  	"github.com/triarius/goreleaser/pkg/config"
    21  	"github.com/triarius/goreleaser/pkg/context"
    22  	"github.com/imdario/mergo"
    23  )
    24  
    25  // Default builder instance.
    26  // nolint: gochecknoglobals
    27  var Default = &Builder{}
    28  
    29  // nolint: gochecknoinits
    30  func init() {
    31  	api.Register("go", Default)
    32  }
    33  
    34  // Builder is golang builder.
    35  type Builder struct{}
    36  
    37  // WithDefaults sets the defaults for a golang build and returns it.
    38  func (*Builder) WithDefaults(build config.Build) (config.Build, error) {
    39  	if build.GoBinary == "" {
    40  		build.GoBinary = "go"
    41  	}
    42  	if build.Command == "" {
    43  		build.Command = "build"
    44  	}
    45  	if build.Dir == "" {
    46  		build.Dir = "."
    47  	}
    48  	if build.Main == "" {
    49  		build.Main = "."
    50  	}
    51  	if len(build.Ldflags) == 0 {
    52  		build.Ldflags = []string{"-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser"}
    53  	}
    54  	if len(build.Targets) == 0 {
    55  		if len(build.Goos) == 0 {
    56  			build.Goos = []string{"linux", "darwin"}
    57  		}
    58  		if len(build.Goarch) == 0 {
    59  			build.Goarch = []string{"amd64", "arm64", "386"}
    60  		}
    61  		if len(build.Goarm) == 0 {
    62  			build.Goarm = []string{"6"}
    63  		}
    64  		if len(build.Gomips) == 0 {
    65  			build.Gomips = []string{"hardfloat"}
    66  		}
    67  		if len(build.Goamd64) == 0 {
    68  			build.Goamd64 = []string{"v1"}
    69  		}
    70  		targets, err := buildtarget.List(build)
    71  		if err != nil {
    72  			return build, err
    73  		}
    74  		build.Targets = targets
    75  	} else {
    76  		targets := map[string]bool{}
    77  		for _, target := range build.Targets {
    78  			if target == go118FirstClassTargetsName ||
    79  				target == goStableFirstClassTargetsName {
    80  				for _, t := range go118FirstClassTargets {
    81  					targets[t] = true
    82  				}
    83  				continue
    84  			}
    85  			if strings.HasSuffix(target, "_amd64") {
    86  				targets[target+"_v1"] = true
    87  				continue
    88  			}
    89  			if strings.HasSuffix(target, "_arm") {
    90  				targets[target+"_6"] = true
    91  				continue
    92  			}
    93  			if strings.HasSuffix(target, "_mips") ||
    94  				strings.HasSuffix(target, "_mips64") ||
    95  				strings.HasSuffix(target, "_mipsle") ||
    96  				strings.HasSuffix(target, "_mips64le") {
    97  				targets[target+"_hardfloat"] = true
    98  				continue
    99  			}
   100  			targets[target] = true
   101  		}
   102  		build.Targets = keys(targets)
   103  	}
   104  	return build, nil
   105  }
   106  
   107  func keys(m map[string]bool) []string {
   108  	result := make([]string, 0, len(m))
   109  	for k := range m {
   110  		result = append(result, k)
   111  	}
   112  	return result
   113  }
   114  
   115  const (
   116  	go118FirstClassTargetsName    = "go_118_first_class"
   117  	goStableFirstClassTargetsName = "go_first_class"
   118  )
   119  
   120  // go tool dist list -json | jq -r '.[] | select(.FirstClass) | [.GOOS, .GOARCH] | @tsv'
   121  var go118FirstClassTargets = []string{
   122  	"darwin_amd64_v1",
   123  	"darwin_arm64",
   124  	"linux_386",
   125  	"linux_amd64_v1",
   126  	"linux_arm_6",
   127  	"linux_arm64",
   128  	"windows_386",
   129  	"windows_amd64_v1",
   130  }
   131  
   132  // Build builds a golang build.
   133  func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error {
   134  	if err := checkMain(build); err != nil {
   135  		return err
   136  	}
   137  
   138  	artifact := &artifact.Artifact{
   139  		Type:    artifact.Binary,
   140  		Path:    options.Path,
   141  		Name:    options.Name,
   142  		Goos:    options.Goos,
   143  		Goarch:  options.Goarch,
   144  		Goamd64: options.Goamd64,
   145  		Goarm:   options.Goarm,
   146  		Gomips:  options.Gomips,
   147  		Extra: map[string]interface{}{
   148  			artifact.ExtraBinary: strings.TrimSuffix(filepath.Base(options.Path), options.Ext),
   149  			artifact.ExtraExt:    options.Ext,
   150  			artifact.ExtraID:     build.ID,
   151  		},
   152  	}
   153  
   154  	details, err := withOverrides(ctx, build, options)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	env := append(ctx.Env.Strings(), details.Env...)
   160  	env = append(
   161  		env,
   162  		"GOOS="+options.Goos,
   163  		"GOARCH="+options.Goarch,
   164  		"GOARM="+options.Goarm,
   165  		"GOMIPS="+options.Gomips,
   166  		"GOMIPS64="+options.Gomips,
   167  		"GOAMD64="+options.Goamd64,
   168  	)
   169  
   170  	cmd, err := buildGoBuildLine(ctx, build, details, options, artifact, env)
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	if err := run(ctx, cmd, env, build.Dir); err != nil {
   176  		return fmt.Errorf("failed to build for %s: %w", options.Target, err)
   177  	}
   178  
   179  	if build.ModTimestamp != "" {
   180  		modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(artifact, map[string]string{}).Apply(build.ModTimestamp)
   181  		if err != nil {
   182  			return err
   183  		}
   184  		modUnix, err := strconv.ParseInt(modTimestamp, 10, 64)
   185  		if err != nil {
   186  			return err
   187  		}
   188  		modTime := time.Unix(modUnix, 0)
   189  		err = os.Chtimes(options.Path, modTime, modTime)
   190  		if err != nil {
   191  			return fmt.Errorf("failed to change times for %s: %w", options.Target, err)
   192  		}
   193  	}
   194  
   195  	ctx.Artifacts.Add(artifact)
   196  	return nil
   197  }
   198  
   199  func withOverrides(ctx *context.Context, build config.Build, options api.Options) (config.BuildDetails, error) {
   200  	optsTarget := options.Goos + options.Goarch + options.Goarm + options.Gomips + options.Goamd64
   201  	for _, o := range build.BuildDetailsOverrides {
   202  		overrideTarget, err := tmpl.New(ctx).Apply(o.Goos + o.Goarch + o.Gomips + o.Goarm + o.Goamd64)
   203  		if err != nil {
   204  			return build.BuildDetails, err
   205  		}
   206  
   207  		if optsTarget == overrideTarget {
   208  			dets := config.BuildDetails{
   209  				Ldflags:  build.BuildDetails.Ldflags,
   210  				Tags:     build.BuildDetails.Tags,
   211  				Flags:    build.BuildDetails.Flags,
   212  				Asmflags: build.BuildDetails.Asmflags,
   213  				Gcflags:  build.BuildDetails.Gcflags,
   214  			}
   215  			if err := mergo.Merge(&dets, o.BuildDetails, mergo.WithOverride); err != nil {
   216  				return build.BuildDetails, err
   217  			}
   218  
   219  			dets.Env = context.ToEnv(append(build.Env, o.BuildDetails.Env...)).Strings()
   220  			log.WithField("details", dets).Infof("overridden build details for %s", optsTarget)
   221  			return dets, nil
   222  		}
   223  	}
   224  
   225  	return build.BuildDetails, nil
   226  }
   227  
   228  func buildGoBuildLine(ctx *context.Context, build config.Build, details config.BuildDetails, options api.Options, artifact *artifact.Artifact, env []string) ([]string, error) {
   229  	cmd := []string{build.GoBinary, build.Command}
   230  
   231  	flags, err := processFlags(ctx, artifact, env, details.Flags, "")
   232  	if err != nil {
   233  		return cmd, err
   234  	}
   235  	cmd = append(cmd, flags...)
   236  
   237  	asmflags, err := processFlags(ctx, artifact, env, details.Asmflags, "-asmflags=")
   238  	if err != nil {
   239  		return cmd, err
   240  	}
   241  	cmd = append(cmd, asmflags...)
   242  
   243  	gcflags, err := processFlags(ctx, artifact, env, details.Gcflags, "-gcflags=")
   244  	if err != nil {
   245  		return cmd, err
   246  	}
   247  	cmd = append(cmd, gcflags...)
   248  
   249  	// tags is not a repeatable flag
   250  	if len(details.Tags) > 0 {
   251  		tags, err := processFlags(ctx, artifact, env, details.Tags, "")
   252  		if err != nil {
   253  			return cmd, err
   254  		}
   255  		cmd = append(cmd, "-tags="+strings.Join(tags, ","))
   256  	}
   257  
   258  	// ldflags is not a repeatable flag
   259  	if len(details.Ldflags) > 0 {
   260  		// flag prefix is skipped because ldflags need to output a single string
   261  		ldflags, err := processFlags(ctx, artifact, env, details.Ldflags, "")
   262  		if err != nil {
   263  			return cmd, err
   264  		}
   265  		// ldflags need to be single string in order to apply correctly
   266  		cmd = append(cmd, "-ldflags="+strings.Join(ldflags, " "))
   267  	}
   268  
   269  	cmd = append(cmd, "-o", options.Path, build.Main)
   270  	return cmd, nil
   271  }
   272  
   273  func processFlags(ctx *context.Context, a *artifact.Artifact, env, flags []string, flagPrefix string) ([]string, error) {
   274  	processed := make([]string, 0, len(flags))
   275  	for _, rawFlag := range flags {
   276  		flag, err := processFlag(ctx, a, env, rawFlag)
   277  		if err != nil {
   278  			return nil, err
   279  		}
   280  		processed = append(processed, flagPrefix+flag)
   281  	}
   282  	return processed, nil
   283  }
   284  
   285  func processFlag(ctx *context.Context, a *artifact.Artifact, env []string, rawFlag string) (string, error) {
   286  	return tmpl.New(ctx).WithEnvS(env).WithArtifact(a, map[string]string{}).Apply(rawFlag)
   287  }
   288  
   289  func run(ctx *context.Context, command, env []string, dir string) error {
   290  	/* #nosec */
   291  	cmd := exec.CommandContext(ctx, command[0], command[1:]...)
   292  	log := log.WithField("env", env).WithField("cmd", command)
   293  	cmd.Env = env
   294  	cmd.Dir = dir
   295  	log.Debug("running")
   296  	if out, err := cmd.CombinedOutput(); err != nil {
   297  		return fmt.Errorf("%w: %s", err, string(out))
   298  	}
   299  	return nil
   300  }
   301  
   302  func checkMain(build config.Build) error {
   303  	if build.NoMainCheck {
   304  		return nil
   305  	}
   306  	main := build.Main
   307  	if build.UnproxiedMain != "" {
   308  		main = build.UnproxiedMain
   309  	}
   310  	dir := build.Dir
   311  	if build.UnproxiedDir != "" {
   312  		dir = build.UnproxiedDir
   313  	}
   314  
   315  	if main == "" {
   316  		main = "."
   317  	}
   318  	if dir != "" {
   319  		main = filepath.Join(dir, main)
   320  	}
   321  	stat, ferr := os.Stat(main)
   322  	if ferr != nil {
   323  		return fmt.Errorf("couldn't find main file: %w", ferr)
   324  	}
   325  	if stat.IsDir() {
   326  		packs, err := parser.ParseDir(token.NewFileSet(), main, nil, 0)
   327  		if err != nil {
   328  			return fmt.Errorf("failed to parse dir: %s: %w", main, err)
   329  		}
   330  		for _, pack := range packs {
   331  			for _, file := range pack.Files {
   332  				if hasMain(file) {
   333  					return nil
   334  				}
   335  			}
   336  		}
   337  		return errNoMain{build.Binary}
   338  	}
   339  	file, err := parser.ParseFile(token.NewFileSet(), main, nil, 0)
   340  	if err != nil {
   341  		return fmt.Errorf("failed to parse file: %s: %w", main, err)
   342  	}
   343  	if hasMain(file) {
   344  		return nil
   345  	}
   346  	return errNoMain{build.Binary}
   347  }
   348  
   349  type errNoMain struct {
   350  	bin string
   351  }
   352  
   353  func (e errNoMain) Error() string {
   354  	return fmt.Sprintf("build for %s does not contain a main function\nLearn more at https://goreleaser.com/errors/no-main\n", e.bin)
   355  }
   356  
   357  func hasMain(file *ast.File) bool {
   358  	for _, decl := range file.Decls {
   359  		fn, isFn := decl.(*ast.FuncDecl)
   360  		if !isFn {
   361  			continue
   362  		}
   363  		if fn.Name.Name == "main" && fn.Recv == nil {
   364  			return true
   365  		}
   366  	}
   367  	return false
   368  }