github.com/ahmet2mir/goreleaser@v0.180.3-0.20210927151101-8e5ee5a9b8c5/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/apex/log"
    16  	"github.com/goreleaser/goreleaser/internal/artifact"
    17  	"github.com/goreleaser/goreleaser/internal/builders/buildtarget"
    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.GoBinary == "" {
    39  		build.GoBinary = "go"
    40  	}
    41  	if build.Dir == "" {
    42  		build.Dir = "."
    43  	}
    44  	if build.Main == "" {
    45  		build.Main = "."
    46  	}
    47  	if len(build.Ldflags) == 0 {
    48  		build.Ldflags = []string{"-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser"}
    49  	}
    50  	if len(build.Targets) == 0 {
    51  		if len(build.Goos) == 0 {
    52  			build.Goos = []string{"linux", "darwin"}
    53  		}
    54  		if len(build.Goarch) == 0 {
    55  			build.Goarch = []string{"amd64", "arm64", "386"}
    56  		}
    57  		if len(build.Goarm) == 0 {
    58  			build.Goarm = []string{"6"}
    59  		}
    60  		if len(build.Gomips) == 0 {
    61  			build.Gomips = []string{"hardfloat"}
    62  		}
    63  		targets, err := buildtarget.List(build)
    64  		build.Targets = targets
    65  		if err != nil {
    66  			return build, err
    67  		}
    68  	}
    69  	return build, nil
    70  }
    71  
    72  // Build builds a golang build.
    73  func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error {
    74  	if err := checkMain(build); err != nil {
    75  		return err
    76  	}
    77  
    78  	artifact := &artifact.Artifact{
    79  		Type:   artifact.Binary,
    80  		Path:   options.Path,
    81  		Name:   options.Name,
    82  		Goos:   options.Goos,
    83  		Goarch: options.Goarch,
    84  		Goarm:  options.Goarm,
    85  		Gomips: options.Gomips,
    86  		Extra: map[string]interface{}{
    87  			"Binary": strings.TrimSuffix(filepath.Base(options.Path), options.Ext),
    88  			"Ext":    options.Ext,
    89  			"ID":     build.ID,
    90  		},
    91  	}
    92  
    93  	env := append(ctx.Env.Strings(), build.Env...)
    94  	env = append(
    95  		env,
    96  		"GOOS="+options.Goos,
    97  		"GOARCH="+options.Goarch,
    98  		"GOARM="+options.Goarm,
    99  		"GOMIPS="+options.Gomips,
   100  		"GOMIPS64="+options.Gomips,
   101  	)
   102  
   103  	cmd, err := buildGoBuildLine(ctx, build, options, artifact, env)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	if err := run(ctx, cmd, env, build.Dir); err != nil {
   109  		return fmt.Errorf("failed to build for %s: %w", options.Target, err)
   110  	}
   111  
   112  	if build.ModTimestamp != "" {
   113  		modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(artifact, map[string]string{}).Apply(build.ModTimestamp)
   114  		if err != nil {
   115  			return err
   116  		}
   117  		modUnix, err := strconv.ParseInt(modTimestamp, 10, 64)
   118  		if err != nil {
   119  			return err
   120  		}
   121  		modTime := time.Unix(modUnix, 0)
   122  		err = os.Chtimes(options.Path, modTime, modTime)
   123  		if err != nil {
   124  			return fmt.Errorf("failed to change times for %s: %w", options.Target, err)
   125  		}
   126  	}
   127  
   128  	ctx.Artifacts.Add(artifact)
   129  	return nil
   130  }
   131  
   132  func buildGoBuildLine(ctx *context.Context, build config.Build, options api.Options, artifact *artifact.Artifact, env []string) ([]string, error) {
   133  	cmd := []string{build.GoBinary, "build"}
   134  	flags, err := processFlags(ctx, artifact, env, build.Flags, "")
   135  	if err != nil {
   136  		return cmd, err
   137  	}
   138  	cmd = append(cmd, flags...)
   139  
   140  	asmflags, err := processFlags(ctx, artifact, env, build.Asmflags, "-asmflags=")
   141  	if err != nil {
   142  		return cmd, err
   143  	}
   144  	cmd = append(cmd, asmflags...)
   145  
   146  	gcflags, err := processFlags(ctx, artifact, env, build.Gcflags, "-gcflags=")
   147  	if err != nil {
   148  		return cmd, err
   149  	}
   150  	cmd = append(cmd, gcflags...)
   151  
   152  	// tags is not a repeatable flag
   153  	if len(build.Tags) > 0 {
   154  		tags, err := processFlags(ctx, artifact, env, build.Tags, "")
   155  		if err != nil {
   156  			return cmd, err
   157  		}
   158  		cmd = append(cmd, "-tags="+strings.Join(tags, ","))
   159  	}
   160  
   161  	// ldflags is not a repeatable flag
   162  	if len(build.Ldflags) > 0 {
   163  		// flag prefix is skipped because ldflags need to output a single string
   164  		ldflags, err := processFlags(ctx, artifact, env, build.Ldflags, "")
   165  		if err != nil {
   166  			return cmd, err
   167  		}
   168  		// ldflags need to be single string in order to apply correctly
   169  		cmd = append(cmd, "-ldflags="+strings.Join(ldflags, " "))
   170  	}
   171  
   172  	cmd = append(cmd, "-o", options.Path, build.Main)
   173  	return cmd, nil
   174  }
   175  
   176  func processFlags(ctx *context.Context, a *artifact.Artifact, env, flags []string, flagPrefix string) ([]string, error) {
   177  	processed := make([]string, 0, len(flags))
   178  	for _, rawFlag := range flags {
   179  		flag, err := processFlag(ctx, a, env, rawFlag)
   180  		if err != nil {
   181  			return nil, err
   182  		}
   183  		processed = append(processed, flagPrefix+flag)
   184  	}
   185  	return processed, nil
   186  }
   187  
   188  func processFlag(ctx *context.Context, a *artifact.Artifact, env []string, rawFlag string) (string, error) {
   189  	return tmpl.New(ctx).WithEnvS(env).WithArtifact(a, map[string]string{}).Apply(rawFlag)
   190  }
   191  
   192  func run(ctx *context.Context, command, env []string, dir string) error {
   193  	/* #nosec */
   194  	cmd := exec.CommandContext(ctx, command[0], command[1:]...)
   195  	log := log.WithField("env", env).WithField("cmd", command)
   196  	cmd.Env = env
   197  	cmd.Dir = dir
   198  	log.Debug("running")
   199  	if out, err := cmd.CombinedOutput(); err != nil {
   200  		return fmt.Errorf("%w: %s", err, string(out))
   201  	}
   202  	return nil
   203  }
   204  
   205  func checkMain(build config.Build) error {
   206  	main := build.Main
   207  	if build.UnproxiedMain != "" {
   208  		main = build.UnproxiedMain
   209  	}
   210  	dir := build.Dir
   211  	if build.UnproxiedDir != "" {
   212  		dir = build.UnproxiedDir
   213  	}
   214  
   215  	if main == "" {
   216  		main = "."
   217  	}
   218  	if dir != "" {
   219  		main = filepath.Join(dir, main)
   220  	}
   221  	stat, ferr := os.Stat(main)
   222  	if ferr != nil {
   223  		return fmt.Errorf("couldn't find main file: %w", ferr)
   224  	}
   225  	if stat.IsDir() {
   226  		packs, err := parser.ParseDir(token.NewFileSet(), main, nil, 0)
   227  		if err != nil {
   228  			return fmt.Errorf("failed to parse dir: %s: %w", main, err)
   229  		}
   230  		for _, pack := range packs {
   231  			for _, file := range pack.Files {
   232  				if hasMain(file) {
   233  					return nil
   234  				}
   235  			}
   236  		}
   237  		return fmt.Errorf("build for %s does not contain a main function", build.Binary)
   238  	}
   239  	file, err := parser.ParseFile(token.NewFileSet(), main, nil, 0)
   240  	if err != nil {
   241  		return fmt.Errorf("failed to parse file: %s: %w", main, err)
   242  	}
   243  	if hasMain(file) {
   244  		return nil
   245  	}
   246  	return fmt.Errorf("build for %s does not contain a main function", build.Binary)
   247  }
   248  
   249  func hasMain(file *ast.File) bool {
   250  	for _, decl := range file.Decls {
   251  		fn, isFn := decl.(*ast.FuncDecl)
   252  		if !isFn {
   253  			continue
   254  		}
   255  		if fn.Name.Name == "main" && fn.Recv == nil {
   256  			return true
   257  		}
   258  	}
   259  	return false
   260  }