github.com/goreleaser/goreleaser@v1.25.1/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  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"reflect"
    10  	"strings"
    11  
    12  	"github.com/caarlos0/go-shellwords"
    13  	"github.com/caarlos0/log"
    14  	"github.com/goreleaser/goreleaser/internal/deprecate"
    15  	"github.com/goreleaser/goreleaser/internal/ids"
    16  	"github.com/goreleaser/goreleaser/internal/semerrgroup"
    17  	"github.com/goreleaser/goreleaser/internal/shell"
    18  	"github.com/goreleaser/goreleaser/internal/skips"
    19  	"github.com/goreleaser/goreleaser/internal/tmpl"
    20  	builders "github.com/goreleaser/goreleaser/pkg/build"
    21  	"github.com/goreleaser/goreleaser/pkg/config"
    22  	"github.com/goreleaser/goreleaser/pkg/context"
    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  	g := semerrgroup.New(ctx.Parallelism)
    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  		runPipeOnBuild(ctx, g, build)
    45  	}
    46  	return g.Wait()
    47  }
    48  
    49  // Default sets the pipe defaults.
    50  func (Pipe) Default(ctx *context.Context) error {
    51  	if !reflect.DeepEqual(ctx.Config.SingleBuild, config.Build{}) {
    52  		deprecate.Notice(ctx, "build")
    53  	}
    54  
    55  	ids := ids.New("builds")
    56  	for i, build := range ctx.Config.Builds {
    57  		build, err := buildWithDefaults(ctx, build)
    58  		if err != nil {
    59  			return err
    60  		}
    61  		ctx.Config.Builds[i] = build
    62  		ids.Inc(ctx.Config.Builds[i].ID)
    63  	}
    64  	if len(ctx.Config.Builds) == 0 {
    65  		build, err := buildWithDefaults(ctx, ctx.Config.SingleBuild)
    66  		if err != nil {
    67  			return err
    68  		}
    69  		ctx.Config.Builds = []config.Build{build}
    70  	}
    71  	return ids.Validate()
    72  }
    73  
    74  func buildWithDefaults(ctx *context.Context, build config.Build) (config.Build, error) {
    75  	if build.Builder == "" {
    76  		build.Builder = "go"
    77  	}
    78  	if build.Binary == "" {
    79  		build.Binary = ctx.Config.ProjectName
    80  	}
    81  	if build.ID == "" {
    82  		build.ID = ctx.Config.ProjectName
    83  	}
    84  	for k, v := range build.Env {
    85  		build.Env[k] = os.ExpandEnv(v)
    86  	}
    87  	return builders.For(build.Builder).WithDefaults(build)
    88  }
    89  
    90  func runPipeOnBuild(ctx *context.Context, g semerrgroup.Group, build config.Build) {
    91  	for _, target := range filter(ctx, build.Targets) {
    92  		target := target
    93  		build := build
    94  		g.Go(func() error {
    95  			opts, err := buildOptionsForTarget(ctx, build, target)
    96  			if err != nil {
    97  				return err
    98  			}
    99  
   100  			if !skips.Any(ctx, skips.PreBuildHooks) {
   101  				if err := runHook(ctx, *opts, build.Env, build.Hooks.Pre); err != nil {
   102  					return fmt.Errorf("pre hook failed: %w", err)
   103  				}
   104  			}
   105  			if err := doBuild(ctx, build, *opts); err != nil {
   106  				return err
   107  			}
   108  			if !skips.Any(ctx, skips.PostBuildHooks) {
   109  				if err := runHook(ctx, *opts, build.Env, build.Hooks.Post); err != nil {
   110  					return fmt.Errorf("post hook failed: %w", err)
   111  				}
   112  			}
   113  			return nil
   114  		})
   115  	}
   116  }
   117  
   118  func runHook(ctx *context.Context, opts builders.Options, buildEnv []string, hooks config.Hooks) error {
   119  	if len(hooks) == 0 {
   120  		return nil
   121  	}
   122  
   123  	for _, hook := range hooks {
   124  		var env []string
   125  
   126  		env = append(env, ctx.Env.Strings()...)
   127  		for _, rawEnv := range append(buildEnv, hook.Env...) {
   128  			e, err := tmpl.New(ctx).WithBuildOptions(opts).Apply(rawEnv)
   129  			if err != nil {
   130  				return err
   131  			}
   132  			env = append(env, e)
   133  		}
   134  
   135  		dir, err := tmpl.New(ctx).WithBuildOptions(opts).Apply(hook.Dir)
   136  		if err != nil {
   137  			return err
   138  		}
   139  
   140  		sh, err := tmpl.New(ctx).WithBuildOptions(opts).
   141  			WithEnvS(env).
   142  			Apply(hook.Cmd)
   143  		if err != nil {
   144  			return err
   145  		}
   146  
   147  		log.WithField("hook", sh).Info("running hook")
   148  		cmd, err := shellwords.Parse(sh)
   149  		if err != nil {
   150  			return err
   151  		}
   152  
   153  		if err := shell.Run(ctx, dir, cmd, env, hook.Output); err != nil {
   154  			return err
   155  		}
   156  	}
   157  
   158  	return nil
   159  }
   160  
   161  func doBuild(ctx *context.Context, build config.Build, opts builders.Options) error {
   162  	return builders.For(build.Builder).Build(ctx, build, opts)
   163  }
   164  
   165  func buildOptionsForTarget(ctx *context.Context, build config.Build, target string) (*builders.Options, error) {
   166  	ext := extFor(target, build.BuildDetails)
   167  	parts := strings.Split(target, "_")
   168  	if len(parts) < 2 {
   169  		return nil, fmt.Errorf("%s is not a valid build target", target)
   170  	}
   171  
   172  	goos := parts[0]
   173  	goarch := parts[1]
   174  
   175  	var gomips string
   176  	var goarm string
   177  	var goamd64 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  	if strings.HasPrefix(goarch, "amd64") && len(parts) > 2 {
   185  		goamd64 = parts[2]
   186  	}
   187  
   188  	buildOpts := builders.Options{
   189  		Target:  target,
   190  		Ext:     ext,
   191  		Goos:    goos,
   192  		Goarch:  goarch,
   193  		Goarm:   goarm,
   194  		Gomips:  gomips,
   195  		Goamd64: goamd64,
   196  	}
   197  
   198  	bin, err := tmpl.New(ctx).WithBuildOptions(buildOpts).Apply(build.Binary)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	name := bin + ext
   204  	dir := fmt.Sprintf("%s_%s", build.ID, target)
   205  	if build.NoUniqueDistDir {
   206  		dir = ""
   207  	}
   208  	relpath := filepath.Join(ctx.Config.Dist, dir, name)
   209  	path, err := filepath.Abs(relpath)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	buildOpts.Path = path
   214  	buildOpts.Name = name
   215  
   216  	log.WithField("binary", relpath).Info("building")
   217  	return &buildOpts, nil
   218  }
   219  
   220  func extFor(target string, build config.BuildDetails) string {
   221  	// Configure the extensions for shared and static libraries - by default .so and .a respectively -
   222  	// with overrides for Windows (.dll for shared and .lib for static) and .dylib for macOS.
   223  	switch build.Buildmode {
   224  	case "c-shared":
   225  		if strings.Contains(target, "darwin") {
   226  			return ".dylib"
   227  		}
   228  		if strings.Contains(target, "windows") {
   229  			return ".dll"
   230  		}
   231  		return ".so"
   232  	case "c-archive":
   233  		if strings.Contains(target, "windows") {
   234  			return ".lib"
   235  		}
   236  		return ".a"
   237  	}
   238  
   239  	if target == "js_wasm" {
   240  		return ".wasm"
   241  	}
   242  
   243  	if strings.Contains(target, "windows") {
   244  		return ".exe"
   245  	}
   246  
   247  	return ""
   248  }