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