github.com/goreleaser/goreleaser@v1.25.1/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  	"slices"
    12  	"strings"
    13  
    14  	"dario.cat/mergo"
    15  	"github.com/caarlos0/log"
    16  	"github.com/goreleaser/goreleaser/internal/artifact"
    17  	"github.com/goreleaser/goreleaser/internal/builders/buildtarget"
    18  	"github.com/goreleaser/goreleaser/internal/gio"
    19  	"github.com/goreleaser/goreleaser/internal/logext"
    20  	"github.com/goreleaser/goreleaser/internal/tmpl"
    21  	api "github.com/goreleaser/goreleaser/pkg/build"
    22  	"github.com/goreleaser/goreleaser/pkg/config"
    23  	"github.com/goreleaser/goreleaser/pkg/context"
    24  )
    25  
    26  // Default builder instance.
    27  // nolint: gochecknoglobals
    28  var Default = &Builder{}
    29  
    30  // nolint: gochecknoinits
    31  func init() {
    32  	api.Register("go", Default)
    33  }
    34  
    35  // Builder is golang builder.
    36  type Builder struct{}
    37  
    38  // WithDefaults sets the defaults for a golang build and returns it.
    39  func (*Builder) WithDefaults(build config.Build) (config.Build, error) {
    40  	if build.GoBinary == "" {
    41  		build.GoBinary = "go"
    42  	}
    43  	if build.Command == "" {
    44  		build.Command = "build"
    45  	}
    46  	if build.Dir == "" {
    47  		build.Dir = "."
    48  	}
    49  	if build.Main == "" {
    50  		build.Main = "."
    51  	}
    52  	if len(build.Ldflags) == 0 {
    53  		build.Ldflags = []string{"-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser"}
    54  	}
    55  
    56  	_ = warnIfTargetsAndOtherOptionTogether(build)
    57  	if len(build.Targets) == 0 {
    58  		if len(build.Goos) == 0 {
    59  			build.Goos = []string{"linux", "darwin", "windows"}
    60  		}
    61  		if len(build.Goarch) == 0 {
    62  			build.Goarch = []string{"amd64", "arm64", "386"}
    63  		}
    64  		if len(build.Goarm) == 0 {
    65  			build.Goarm = []string{"6"}
    66  		}
    67  		if len(build.Gomips) == 0 {
    68  			build.Gomips = []string{"hardfloat"}
    69  		}
    70  		if len(build.Goamd64) == 0 {
    71  			build.Goamd64 = []string{"v1"}
    72  		}
    73  		targets, err := buildtarget.List(build)
    74  		if err != nil {
    75  			return build, err
    76  		}
    77  		build.Targets = targets
    78  	} else {
    79  		targets := map[string]bool{}
    80  		for _, target := range build.Targets {
    81  			if target == go118FirstClassTargetsName ||
    82  				target == goStableFirstClassTargetsName {
    83  				for _, t := range go118FirstClassTargets {
    84  					targets[t] = true
    85  				}
    86  				continue
    87  			}
    88  			if strings.HasSuffix(target, "_amd64") {
    89  				targets[target+"_v1"] = true
    90  				continue
    91  			}
    92  			if strings.HasSuffix(target, "_arm") {
    93  				targets[target+"_6"] = true
    94  				continue
    95  			}
    96  			if strings.HasSuffix(target, "_mips") ||
    97  				strings.HasSuffix(target, "_mips64") ||
    98  				strings.HasSuffix(target, "_mipsle") ||
    99  				strings.HasSuffix(target, "_mips64le") {
   100  				targets[target+"_hardfloat"] = true
   101  				continue
   102  			}
   103  			targets[target] = true
   104  		}
   105  		build.Targets = keys(targets)
   106  	}
   107  	return build, nil
   108  }
   109  
   110  func warnIfTargetsAndOtherOptionTogether(build config.Build) bool {
   111  	if len(build.Targets) == 0 {
   112  		return false
   113  	}
   114  
   115  	res := false
   116  	for k, v := range map[string]int{
   117  		"goos":    len(build.Goos),
   118  		"goarch":  len(build.Goarch),
   119  		"goarm":   len(build.Goarm),
   120  		"gomips":  len(build.Gomips),
   121  		"goamd64": len(build.Goamd64),
   122  		"ignore":  len(build.Ignore),
   123  	} {
   124  		if v == 0 {
   125  			continue
   126  		}
   127  		log.Warnf(logext.Keyword("builds."+k) + " is ignored when " + logext.Keyword("builds.targets") + " is set")
   128  		res = true
   129  	}
   130  	return res
   131  }
   132  
   133  func keys(m map[string]bool) []string {
   134  	result := make([]string, 0, len(m))
   135  	for k := range m {
   136  		result = append(result, k)
   137  	}
   138  	return result
   139  }
   140  
   141  const (
   142  	go118FirstClassTargetsName    = "go_118_first_class"
   143  	goStableFirstClassTargetsName = "go_first_class"
   144  )
   145  
   146  // go tool dist list -json | jq -r '.[] | select(.FirstClass) | [.GOOS, .GOARCH] | @tsv'
   147  var go118FirstClassTargets = []string{
   148  	"darwin_amd64_v1",
   149  	"darwin_arm64",
   150  	"linux_386",
   151  	"linux_amd64_v1",
   152  	"linux_arm_6",
   153  	"linux_arm64",
   154  	"windows_386",
   155  	"windows_amd64_v1",
   156  }
   157  
   158  // Build builds a golang build.
   159  func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error {
   160  	if err := checkMain(build); err != nil {
   161  		return err
   162  	}
   163  
   164  	a := &artifact.Artifact{
   165  		Type:    artifact.Binary,
   166  		Path:    options.Path,
   167  		Name:    options.Name,
   168  		Goos:    options.Goos,
   169  		Goarch:  options.Goarch,
   170  		Goamd64: options.Goamd64,
   171  		Goarm:   options.Goarm,
   172  		Gomips:  options.Gomips,
   173  		Extra: map[string]interface{}{
   174  			artifact.ExtraBinary: strings.TrimSuffix(filepath.Base(options.Path), options.Ext),
   175  			artifact.ExtraExt:    options.Ext,
   176  			artifact.ExtraID:     build.ID,
   177  		},
   178  	}
   179  
   180  	if build.Buildmode == "c-archive" {
   181  		a.Type = artifact.CArchive
   182  		ctx.Artifacts.Add(getHeaderArtifactForLibrary(build, options))
   183  	}
   184  	if build.Buildmode == "c-shared" {
   185  		a.Type = artifact.CShared
   186  		ctx.Artifacts.Add(getHeaderArtifactForLibrary(build, options))
   187  	}
   188  
   189  	details, err := withOverrides(ctx, build, options)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	env := []string{}
   195  	// used for unit testing only
   196  	testEnvs := []string{}
   197  	env = append(env, ctx.Env.Strings()...)
   198  	for _, e := range details.Env {
   199  		ee, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(a).Apply(e)
   200  		if err != nil {
   201  			return err
   202  		}
   203  		log.Debugf("env %q evaluated to %q", e, ee)
   204  		if ee != "" {
   205  			env = append(env, ee)
   206  			if strings.HasPrefix(e, "TEST_") {
   207  				testEnvs = append(testEnvs, ee)
   208  			}
   209  		}
   210  	}
   211  	env = append(
   212  		env,
   213  		"GOOS="+options.Goos,
   214  		"GOARCH="+options.Goarch,
   215  		"GOARM="+options.Goarm,
   216  		"GOMIPS="+options.Gomips,
   217  		"GOMIPS64="+options.Gomips,
   218  		"GOAMD64="+options.Goamd64,
   219  	)
   220  
   221  	if len(testEnvs) > 0 {
   222  		a.Extra["testEnvs"] = testEnvs
   223  	}
   224  
   225  	cmd, err := buildGoBuildLine(ctx, build, details, options, a, env)
   226  	if err != nil {
   227  		return err
   228  	}
   229  
   230  	if err := run(ctx, cmd, env, build.Dir); err != nil {
   231  		return fmt.Errorf("failed to build for %s: %w", options.Target, err)
   232  	}
   233  
   234  	modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(a).Apply(build.ModTimestamp)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	if err := gio.Chtimes(options.Path, modTimestamp); err != nil {
   239  		return err
   240  	}
   241  
   242  	ctx.Artifacts.Add(a)
   243  	return nil
   244  }
   245  
   246  func withOverrides(ctx *context.Context, build config.Build, options api.Options) (config.BuildDetails, error) {
   247  	optsTarget := options.Goos + options.Goarch + options.Goarm + options.Gomips + options.Goamd64
   248  	for _, o := range build.BuildDetailsOverrides {
   249  		overrideTarget, err := tmpl.New(ctx).Apply(o.Goos + o.Goarch + o.Gomips + o.Goarm + o.Goamd64)
   250  		if err != nil {
   251  			return build.BuildDetails, err
   252  		}
   253  
   254  		if optsTarget == overrideTarget {
   255  			dets := config.BuildDetails{
   256  				Buildmode: build.BuildDetails.Buildmode,
   257  				Ldflags:   build.BuildDetails.Ldflags,
   258  				Tags:      build.BuildDetails.Tags,
   259  				Flags:     build.BuildDetails.Flags,
   260  				Asmflags:  build.BuildDetails.Asmflags,
   261  				Gcflags:   build.BuildDetails.Gcflags,
   262  			}
   263  			if err := mergo.Merge(&dets, o.BuildDetails, mergo.WithOverride); err != nil {
   264  				return build.BuildDetails, err
   265  			}
   266  
   267  			dets.Env = context.ToEnv(append(build.Env, o.BuildDetails.Env...)).Strings()
   268  			log.WithField("details", dets).Infof("overridden build details for %s", optsTarget)
   269  			return dets, nil
   270  		}
   271  	}
   272  
   273  	return build.BuildDetails, nil
   274  }
   275  
   276  func buildGoBuildLine(
   277  	ctx *context.Context,
   278  	build config.Build,
   279  	details config.BuildDetails,
   280  	options api.Options,
   281  	artifact *artifact.Artifact,
   282  	env []string,
   283  ) ([]string, error) {
   284  	gobin, err := tmpl.New(ctx).WithBuildOptions(options).Apply(build.GoBinary)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	cmd := []string{gobin, build.Command}
   289  
   290  	// tags, ldflags, and buildmode, should only appear once, warning only to avoid a breaking change
   291  	validateUniqueFlags(details)
   292  
   293  	flags, err := processFlags(ctx, artifact, env, details.Flags, "")
   294  	if err != nil {
   295  		return cmd, err
   296  	}
   297  	cmd = append(cmd, flags...)
   298  	if build.Command == "test" && !slices.Contains(flags, "-c") {
   299  		cmd = append(cmd, "-c")
   300  	}
   301  
   302  	asmflags, err := processFlags(ctx, artifact, env, details.Asmflags, "-asmflags=")
   303  	if err != nil {
   304  		return cmd, err
   305  	}
   306  	cmd = append(cmd, asmflags...)
   307  
   308  	gcflags, err := processFlags(ctx, artifact, env, details.Gcflags, "-gcflags=")
   309  	if err != nil {
   310  		return cmd, err
   311  	}
   312  	cmd = append(cmd, gcflags...)
   313  
   314  	// tags is not a repeatable flag
   315  	if len(details.Tags) > 0 {
   316  		tags, err := processFlags(ctx, artifact, env, details.Tags, "")
   317  		if err != nil {
   318  			return cmd, err
   319  		}
   320  		cmd = append(cmd, "-tags="+strings.Join(tags, ","))
   321  	}
   322  
   323  	// ldflags is not a repeatable flag
   324  	if len(details.Ldflags) > 0 {
   325  		// flag prefix is skipped because ldflags need to output a single string
   326  		ldflags, err := processFlags(ctx, artifact, env, details.Ldflags, "")
   327  		if err != nil {
   328  			return cmd, err
   329  		}
   330  		// ldflags need to be single string in order to apply correctly
   331  		cmd = append(cmd, "-ldflags="+strings.Join(ldflags, " "))
   332  	}
   333  
   334  	if details.Buildmode != "" {
   335  		cmd = append(cmd, "-buildmode="+details.Buildmode)
   336  	}
   337  
   338  	cmd = append(cmd, "-o", options.Path, build.Main)
   339  	return cmd, nil
   340  }
   341  
   342  func validateUniqueFlags(details config.BuildDetails) {
   343  	for _, flag := range details.Flags {
   344  		if strings.HasPrefix(flag, "-tags") && len(details.Tags) > 0 {
   345  			log.WithField("flag", flag).WithField("tags", details.Tags).Warn("tags is defined twice")
   346  		}
   347  		if strings.HasPrefix(flag, "-ldflags") && len(details.Ldflags) > 0 {
   348  			log.WithField("flag", flag).WithField("ldflags", details.Ldflags).Warn("ldflags is defined twice")
   349  		}
   350  		if strings.HasPrefix(flag, "-buildmode") && details.Buildmode != "" {
   351  			log.WithField("flag", flag).WithField("buildmode", details.Buildmode).Warn("buildmode is defined twice")
   352  		}
   353  	}
   354  }
   355  
   356  func processFlags(ctx *context.Context, a *artifact.Artifact, env, flags []string, flagPrefix string) ([]string, error) {
   357  	processed := make([]string, 0, len(flags))
   358  	for _, rawFlag := range flags {
   359  		flag, err := processFlag(ctx, a, env, rawFlag)
   360  		if err != nil {
   361  			return nil, err
   362  		}
   363  		processed = append(processed, flagPrefix+flag)
   364  	}
   365  	return processed, nil
   366  }
   367  
   368  func processFlag(ctx *context.Context, a *artifact.Artifact, env []string, rawFlag string) (string, error) {
   369  	return tmpl.New(ctx).WithEnvS(env).WithArtifact(a).Apply(rawFlag)
   370  }
   371  
   372  func run(ctx *context.Context, command, env []string, dir string) error {
   373  	/* #nosec */
   374  	cmd := exec.CommandContext(ctx, command[0], command[1:]...)
   375  	log := log.WithField("env", env).WithField("cmd", command)
   376  	cmd.Env = env
   377  	cmd.Dir = dir
   378  	log.Debug("running")
   379  	out, err := cmd.CombinedOutput()
   380  	if err != nil {
   381  		return fmt.Errorf("%w: %s", err, string(out))
   382  	}
   383  	log.Debug(string(out))
   384  	return nil
   385  }
   386  
   387  func checkMain(build config.Build) error {
   388  	if build.NoMainCheck {
   389  		return nil
   390  	}
   391  	main := build.Main
   392  	if build.UnproxiedMain != "" {
   393  		main = build.UnproxiedMain
   394  	}
   395  	dir := build.Dir
   396  	if build.UnproxiedDir != "" {
   397  		dir = build.UnproxiedDir
   398  	}
   399  
   400  	if main == "" {
   401  		main = "."
   402  	}
   403  	if dir != "" {
   404  		main = filepath.Join(dir, main)
   405  	}
   406  	stat, ferr := os.Stat(main)
   407  	if ferr != nil {
   408  		return fmt.Errorf("couldn't find main file: %w", ferr)
   409  	}
   410  	if stat.IsDir() {
   411  		packs, err := parser.ParseDir(token.NewFileSet(), main, nil, 0)
   412  		if err != nil {
   413  			return fmt.Errorf("failed to parse dir: %s: %w", main, err)
   414  		}
   415  		for _, pack := range packs {
   416  			for _, file := range pack.Files {
   417  				if hasMain(file) {
   418  					return nil
   419  				}
   420  			}
   421  		}
   422  		return errNoMain{build.Binary}
   423  	}
   424  	file, err := parser.ParseFile(token.NewFileSet(), main, nil, 0)
   425  	if err != nil {
   426  		return fmt.Errorf("failed to parse file: %s: %w", main, err)
   427  	}
   428  	if hasMain(file) {
   429  		return nil
   430  	}
   431  	return errNoMain{build.Binary}
   432  }
   433  
   434  type errNoMain struct {
   435  	bin string
   436  }
   437  
   438  func (e errNoMain) Error() string {
   439  	return fmt.Sprintf("build for %s does not contain a main function\nLearn more at https://goreleaser.com/errors/no-main\n", e.bin)
   440  }
   441  
   442  func hasMain(file *ast.File) bool {
   443  	for _, decl := range file.Decls {
   444  		fn, isFn := decl.(*ast.FuncDecl)
   445  		if !isFn {
   446  			continue
   447  		}
   448  		if fn.Name.Name == "main" && fn.Recv == nil {
   449  			return true
   450  		}
   451  	}
   452  	return false
   453  }
   454  
   455  func getHeaderArtifactForLibrary(build config.Build, options api.Options) *artifact.Artifact {
   456  	fullPathWithoutExt := strings.TrimSuffix(options.Path, options.Ext)
   457  	basePath := filepath.Base(fullPathWithoutExt)
   458  	fullPath := fullPathWithoutExt + ".h"
   459  	headerName := basePath + ".h"
   460  
   461  	return &artifact.Artifact{
   462  		Type:    artifact.Header,
   463  		Path:    fullPath,
   464  		Name:    headerName,
   465  		Goos:    options.Goos,
   466  		Goarch:  options.Goarch,
   467  		Goamd64: options.Goamd64,
   468  		Goarm:   options.Goarm,
   469  		Gomips:  options.Gomips,
   470  		Extra: map[string]interface{}{
   471  			artifact.ExtraBinary: headerName,
   472  			artifact.ExtraExt:    ".h",
   473  			artifact.ExtraID:     build.ID,
   474  		},
   475  	}
   476  }