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