github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/internal/builders/golang/build.go (about)

     1  package golang
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"go/ast"
     7  	"go/parser"
     8  	"go/token"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/apex/log"
    17  	"github.com/goreleaser/goreleaser/internal/artifact"
    18  	"github.com/goreleaser/goreleaser/internal/tmpl"
    19  	api "github.com/goreleaser/goreleaser/pkg/build"
    20  	"github.com/goreleaser/goreleaser/pkg/config"
    21  	"github.com/goreleaser/goreleaser/pkg/context"
    22  )
    23  
    24  // Default builder instance.
    25  // nolint: gochecknoglobals
    26  var Default = &Builder{}
    27  
    28  // nolint: gochecknoinits
    29  func init() {
    30  	api.Register("go", Default)
    31  }
    32  
    33  // Builder is golang builder.
    34  type Builder struct{}
    35  
    36  // WithDefaults sets the defaults for a golang build and returns it.
    37  func (*Builder) WithDefaults(build config.Build) (config.Build, error) {
    38  	if build.Dir == "" {
    39  		build.Dir = "."
    40  	}
    41  	if build.Main == "" {
    42  		build.Main = "."
    43  	}
    44  	if len(build.Ldflags) == 0 {
    45  		build.Ldflags = []string{"-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser"}
    46  	}
    47  	if len(build.Targets) == 0 {
    48  		if len(build.Goos) == 0 {
    49  			build.Goos = []string{"linux", "darwin"}
    50  		}
    51  		if len(build.Goarch) == 0 {
    52  			build.Goarch = []string{"amd64", "arm64", "386"}
    53  		}
    54  		if len(build.Goarm) == 0 {
    55  			build.Goarm = []string{"6"}
    56  		}
    57  		targets, err := matrix(build)
    58  		build.Targets = targets
    59  		if err != nil {
    60  			return build, err
    61  		}
    62  	}
    63  	if build.GoBinary == "" {
    64  		build.GoBinary = "go"
    65  	}
    66  	return build, nil
    67  }
    68  
    69  // Build builds a golang build.
    70  func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error {
    71  	if err := checkMain(build); err != nil {
    72  		return err
    73  	}
    74  	target, err := newBuildTarget(options.Target)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	var cmd = []string{build.GoBinary, "build"}
    80  
    81  	var env = append(ctx.Env.Strings(), build.Env...)
    82  	env = append(env, target.Env()...)
    83  
    84  	artifact := &artifact.Artifact{
    85  		Type:   artifact.Binary,
    86  		Path:   options.Path,
    87  		Name:   options.Name,
    88  		Goos:   target.os,
    89  		Goarch: target.arch,
    90  		Goarm:  target.arm,
    91  		Gomips: target.mips,
    92  		Extra: map[string]interface{}{
    93  			"Binary": strings.TrimSuffix(filepath.Base(options.Path), options.Ext),
    94  			"Ext":    options.Ext,
    95  			"ID":     build.ID,
    96  		},
    97  	}
    98  
    99  	flags, err := processFlags(ctx, artifact, env, build.Flags, "")
   100  	if err != nil {
   101  		return err
   102  	}
   103  	cmd = append(cmd, flags...)
   104  
   105  	asmflags, err := processFlags(ctx, artifact, env, build.Asmflags, "-asmflags=")
   106  	if err != nil {
   107  		return err
   108  	}
   109  	cmd = append(cmd, asmflags...)
   110  
   111  	gcflags, err := processFlags(ctx, artifact, env, build.Gcflags, "-gcflags=")
   112  	if err != nil {
   113  		return err
   114  	}
   115  	cmd = append(cmd, gcflags...)
   116  
   117  	// flag prefix is skipped because ldflags need to output a single string
   118  	ldflags, err := processFlags(ctx, artifact, env, build.Ldflags, "")
   119  	if err != nil {
   120  		return err
   121  	}
   122  	// ldflags need to be single string in order to apply correctly
   123  	processedLdFlags := joinLdFlags(ldflags)
   124  
   125  	cmd = append(cmd, processedLdFlags)
   126  
   127  	cmd = append(cmd, "-o", options.Path, build.Main)
   128  	if err := run(ctx, cmd, env, build.Dir); err != nil {
   129  		return fmt.Errorf("failed to build for %s: %w", options.Target, err)
   130  	}
   131  
   132  	if build.ModTimestamp != "" {
   133  		modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(artifact, map[string]string{}).Apply(build.ModTimestamp)
   134  		if err != nil {
   135  			return err
   136  		}
   137  		modUnix, err := strconv.ParseInt(modTimestamp, 10, 64)
   138  		if err != nil {
   139  			return err
   140  		}
   141  		modTime := time.Unix(modUnix, 0)
   142  		err = os.Chtimes(options.Path, modTime, modTime)
   143  		if err != nil {
   144  			return fmt.Errorf("failed to change times for %s: %w", options.Target, err)
   145  		}
   146  	}
   147  
   148  	ctx.Artifacts.Add(artifact)
   149  	return nil
   150  }
   151  
   152  func processFlags(ctx *context.Context, a *artifact.Artifact, env, flags []string, flagPrefix string) ([]string, error) {
   153  	processed := make([]string, 0, len(flags))
   154  	for _, rawFlag := range flags {
   155  		flag, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(a, map[string]string{}).Apply(rawFlag)
   156  		if err != nil {
   157  			return nil, err
   158  		}
   159  		processed = append(processed, flagPrefix+flag)
   160  	}
   161  	return processed, nil
   162  }
   163  
   164  func joinLdFlags(flags []string) string {
   165  	ldflagString := strings.Builder{}
   166  	ldflagString.WriteString("-ldflags=")
   167  	ldflagString.WriteString(strings.Join(flags, " "))
   168  
   169  	return ldflagString.String()
   170  }
   171  
   172  func run(ctx *context.Context, command, env []string, dir string) error {
   173  	/* #nosec */
   174  	var cmd = exec.CommandContext(ctx, command[0], command[1:]...)
   175  	var log = log.WithField("env", env).WithField("cmd", command)
   176  	cmd.Env = env
   177  	cmd.Dir = dir
   178  	log.Debug("running")
   179  	if out, err := cmd.CombinedOutput(); err != nil {
   180  		log.WithError(err).Debug("failed")
   181  		return errors.New(string(out))
   182  	}
   183  	return nil
   184  }
   185  
   186  type buildTarget struct {
   187  	os, arch, arm, mips string
   188  }
   189  
   190  func newBuildTarget(s string) (buildTarget, error) {
   191  	var t = buildTarget{}
   192  	parts := strings.Split(s, "_")
   193  	if len(parts) < 2 {
   194  		return t, fmt.Errorf("%s is not a valid build target", s)
   195  	}
   196  	t.os = parts[0]
   197  	t.arch = parts[1]
   198  	if strings.HasPrefix(t.arch, "arm") && len(parts) == 3 {
   199  		t.arm = parts[2]
   200  	}
   201  	if strings.HasPrefix(t.arch, "mips") && len(parts) == 3 {
   202  		t.mips = parts[2]
   203  	}
   204  	return t, nil
   205  }
   206  
   207  func (b buildTarget) Env() []string {
   208  	return []string{
   209  		"GOOS=" + b.os,
   210  		"GOARCH=" + b.arch,
   211  		"GOARM=" + b.arm,
   212  		"GOMIPS=" + b.mips,
   213  		"GOMIPS64=" + b.mips,
   214  	}
   215  }
   216  
   217  func checkMain(build config.Build) error {
   218  	var main = build.Main
   219  	if main == "" {
   220  		main = "."
   221  	}
   222  	if build.Dir != "" {
   223  		main = filepath.Join(build.Dir, main)
   224  	}
   225  	stat, ferr := os.Stat(main)
   226  	if ferr != nil {
   227  		return ferr
   228  	}
   229  	if stat.IsDir() {
   230  		packs, err := parser.ParseDir(token.NewFileSet(), main, fileFilter, 0)
   231  		if err != nil {
   232  			return fmt.Errorf("failed to parse dir: %s: %w", main, err)
   233  		}
   234  		for _, pack := range packs {
   235  			for _, file := range pack.Files {
   236  				if hasMain(file) {
   237  					return nil
   238  				}
   239  			}
   240  		}
   241  		return fmt.Errorf("build for %s does not contain a main function", build.Binary)
   242  	}
   243  	file, err := parser.ParseFile(token.NewFileSet(), main, nil, 0)
   244  	if err != nil {
   245  		return fmt.Errorf("failed to parse file: %s: %w", main, err)
   246  	}
   247  	if hasMain(file) {
   248  		return nil
   249  	}
   250  	return fmt.Errorf("build for %s does not contain a main function", build.Binary)
   251  }
   252  
   253  // TODO: can be removed once we migrate from go 1.15 to 1.16.
   254  func fileFilter(info os.FileInfo) bool {
   255  	return !info.IsDir()
   256  }
   257  
   258  func hasMain(file *ast.File) bool {
   259  	for _, decl := range file.Decls {
   260  		fn, isFn := decl.(*ast.FuncDecl)
   261  		if !isFn {
   262  			continue
   263  		}
   264  		if fn.Name.Name == "main" && fn.Recv == nil {
   265  			return true
   266  		}
   267  	}
   268  	return false
   269  }