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

     1  // Package build provides a pipe that can build binaries for several
     2  // languages.
     3  package build
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"github.com/apex/log"
    15  	"github.com/goreleaser/goreleaser/internal/ids"
    16  	"github.com/goreleaser/goreleaser/internal/logext"
    17  	"github.com/goreleaser/goreleaser/internal/semerrgroup"
    18  	"github.com/goreleaser/goreleaser/internal/tmpl"
    19  	builders "github.com/goreleaser/goreleaser/pkg/build"
    20  	"github.com/goreleaser/goreleaser/pkg/config"
    21  	"github.com/goreleaser/goreleaser/pkg/context"
    22  	"github.com/mattn/go-shellwords"
    23  
    24  	// langs to init.
    25  	_ "github.com/goreleaser/goreleaser/internal/builders/golang"
    26  )
    27  
    28  // Pipe for build.
    29  type Pipe struct{}
    30  
    31  func (Pipe) String() string {
    32  	return "building binaries"
    33  }
    34  
    35  // Run the pipe.
    36  func (Pipe) Run(ctx *context.Context) error {
    37  	for _, build := range ctx.Config.Builds {
    38  		if build.Skip {
    39  			log.WithField("id", build.ID).Info("skip is set")
    40  			continue
    41  		}
    42  		log.WithField("build", build).Debug("building")
    43  		if err := runPipeOnBuild(ctx, build); err != nil {
    44  			return err
    45  		}
    46  	}
    47  	return nil
    48  }
    49  
    50  // Default sets the pipe defaults.
    51  func (Pipe) Default(ctx *context.Context) error {
    52  	var ids = ids.New("builds")
    53  	for i, build := range ctx.Config.Builds {
    54  		build, err := buildWithDefaults(ctx, build)
    55  		if err != nil {
    56  			return err
    57  		}
    58  		ctx.Config.Builds[i] = build
    59  		ids.Inc(ctx.Config.Builds[i].ID)
    60  	}
    61  	if len(ctx.Config.Builds) == 0 {
    62  		build, err := buildWithDefaults(ctx, ctx.Config.SingleBuild)
    63  		if err != nil {
    64  			return err
    65  		}
    66  		ctx.Config.Builds = []config.Build{build}
    67  	}
    68  	return ids.Validate()
    69  }
    70  
    71  func buildWithDefaults(ctx *context.Context, build config.Build) (config.Build, error) {
    72  	if build.Lang == "" {
    73  		build.Lang = "go"
    74  	}
    75  	if build.Binary == "" {
    76  		build.Binary = ctx.Config.ProjectName
    77  	}
    78  	if build.ID == "" {
    79  		build.ID = ctx.Config.ProjectName
    80  	}
    81  	for k, v := range build.Env {
    82  		build.Env[k] = os.ExpandEnv(v)
    83  	}
    84  	return builders.For(build.Lang).WithDefaults(build)
    85  }
    86  
    87  func shouldBuildID(ctx *context.Context, buildID string) bool {
    88  	buildIDs := ctx.BuildIDs
    89  	if len(buildIDs) > 0 {
    90  		for _, ID := range buildIDs {
    91  			if buildID == strings.TrimSpace(ID) {
    92  				return true
    93  			}
    94  		}
    95  		return false
    96  	}
    97  	return true
    98  }
    99  
   100  func runPipeOnBuild(ctx *context.Context, build config.Build) error {
   101  	var g = semerrgroup.New(ctx.Parallelism)
   102  	for _, target := range build.Targets {
   103  		target := target
   104  		build := build
   105  		g.Go(func() error {
   106  			opts, err := buildOptionsForTarget(ctx, build, target)
   107  			if err != nil {
   108  				return err
   109  			}
   110  
   111  			if !shouldBuildID(ctx, build.ID) {
   112  				return nil
   113  			}
   114  
   115  			if err := runHook(ctx, *opts, build.Env, build.Hooks.Pre); err != nil {
   116  				return fmt.Errorf("pre hook failed: %w", err)
   117  			}
   118  			if err := doBuild(ctx, build, *opts); err != nil {
   119  				return err
   120  			}
   121  			if !ctx.SkipPostBuildHooks {
   122  				if err := runHook(ctx, *opts, build.Env, build.Hooks.Post); err != nil {
   123  					return fmt.Errorf("post hook failed: %w", err)
   124  				}
   125  			}
   126  			return nil
   127  		})
   128  	}
   129  
   130  	return g.Wait()
   131  }
   132  
   133  func runHook(ctx *context.Context, opts builders.Options, buildEnv []string, hooks config.BuildHooks) error {
   134  	if len(hooks) == 0 {
   135  		return nil
   136  	}
   137  
   138  	for _, hook := range hooks {
   139  		var env []string
   140  
   141  		env = append(env, ctx.Env.Strings()...)
   142  		env = append(env, buildEnv...)
   143  
   144  		for _, rawEnv := range hook.Env {
   145  			e, err := tmpl.New(ctx).WithBuildOptions(opts).Apply(rawEnv)
   146  			if err != nil {
   147  				return err
   148  			}
   149  			env = append(env, e)
   150  		}
   151  
   152  		dir, err := tmpl.New(ctx).WithBuildOptions(opts).Apply(hook.Dir)
   153  		if err != nil {
   154  			return err
   155  		}
   156  
   157  		sh, err := tmpl.New(ctx).WithBuildOptions(opts).
   158  			WithEnvS(env).
   159  			Apply(hook.Cmd)
   160  		if err != nil {
   161  			return err
   162  		}
   163  
   164  		log.WithField("hook", sh).Info("running hook")
   165  		cmd, err := shellwords.Parse(sh)
   166  		if err != nil {
   167  			return err
   168  		}
   169  
   170  		if err := run(ctx, dir, cmd, env); err != nil {
   171  			return err
   172  		}
   173  	}
   174  
   175  	return nil
   176  }
   177  
   178  func doBuild(ctx *context.Context, build config.Build, opts builders.Options) error {
   179  	return builders.For(build.Lang).Build(ctx, build, opts)
   180  }
   181  
   182  func buildOptionsForTarget(ctx *context.Context, build config.Build, target string) (*builders.Options, error) {
   183  	var ext = extFor(target, build.Flags)
   184  	var goos string
   185  	var goarch string
   186  
   187  	if strings.Contains(target, "_") {
   188  		goos = strings.Split(target, "_")[0]
   189  		goarch = strings.Split(target, "_")[1]
   190  	}
   191  
   192  	buildOpts := builders.Options{
   193  		Target: target,
   194  		Ext:    ext,
   195  		Os:     goos,
   196  		Arch:   goarch,
   197  	}
   198  
   199  	binary, err := tmpl.New(ctx).WithBuildOptions(buildOpts).Apply(build.Binary)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  
   204  	build.Binary = binary
   205  	var name = build.Binary + ext
   206  	path, err := filepath.Abs(
   207  		filepath.Join(
   208  			ctx.Config.Dist,
   209  			fmt.Sprintf("%s_%s", build.ID, target),
   210  			name,
   211  		),
   212  	)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	log.WithField("binary", path).Info("building")
   218  	buildOpts.Name = name
   219  	buildOpts.Path = path
   220  	return &buildOpts, nil
   221  }
   222  
   223  func extFor(target string, flags config.FlagArray) string {
   224  	if strings.Contains(target, "windows") {
   225  		for _, s := range flags {
   226  			if s == "-buildmode=c-shared" {
   227  				return ".dll"
   228  			}
   229  			if s == "-buildmode=c-archive" {
   230  				return ".lib"
   231  			}
   232  		}
   233  		return ".exe"
   234  	}
   235  	if target == "js_wasm" {
   236  		return ".wasm"
   237  	}
   238  	return ""
   239  }
   240  
   241  func run(ctx *context.Context, dir string, command, env []string) error {
   242  	/* #nosec */
   243  	var cmd = exec.CommandContext(ctx, command[0], command[1:]...)
   244  	var entry = log.WithField("cmd", command)
   245  	cmd.Env = env
   246  	var b bytes.Buffer
   247  	cmd.Stderr = io.MultiWriter(logext.NewErrWriter(entry), &b)
   248  	cmd.Stdout = io.MultiWriter(logext.NewWriter(entry), &b)
   249  	if dir != "" {
   250  		cmd.Dir = dir
   251  	}
   252  	entry.WithField("env", env).Debug("running")
   253  	if err := cmd.Run(); err != nil {
   254  		entry.WithError(err).Debug("failed")
   255  		return fmt.Errorf("%q: %w", b.String(), err)
   256  	}
   257  	return nil
   258  }