github.com/goreleaser/goreleaser@v1.25.1/cmd/build.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"runtime"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/caarlos0/ctrlc"
    11  	"github.com/caarlos0/log"
    12  	"github.com/goreleaser/goreleaser/internal/artifact"
    13  	"github.com/goreleaser/goreleaser/internal/deprecate"
    14  	"github.com/goreleaser/goreleaser/internal/gio"
    15  	"github.com/goreleaser/goreleaser/internal/logext"
    16  	"github.com/goreleaser/goreleaser/internal/middleware/errhandler"
    17  	"github.com/goreleaser/goreleaser/internal/middleware/logging"
    18  	"github.com/goreleaser/goreleaser/internal/middleware/skip"
    19  	"github.com/goreleaser/goreleaser/internal/pipeline"
    20  	"github.com/goreleaser/goreleaser/internal/skips"
    21  	"github.com/goreleaser/goreleaser/pkg/config"
    22  	"github.com/goreleaser/goreleaser/pkg/context"
    23  	"github.com/spf13/cobra"
    24  )
    25  
    26  type buildCmd struct {
    27  	cmd  *cobra.Command
    28  	opts buildOpts
    29  }
    30  
    31  type buildOpts struct {
    32  	config       string
    33  	ids          []string
    34  	snapshot     bool
    35  	clean        bool
    36  	deprecated   bool
    37  	parallelism  int
    38  	timeout      time.Duration
    39  	singleTarget bool
    40  	output       string
    41  	skips        []string
    42  
    43  	// Deprecated: use clean instead.
    44  	rmDist bool
    45  	// Deprecated: use skip instead.
    46  	skipValidate bool
    47  	// Deprecated: use skip instead.
    48  	skipBefore bool
    49  	// Deprecated: use skip instead.
    50  	skipPostHooks bool
    51  }
    52  
    53  func newBuildCmd() *buildCmd {
    54  	root := &buildCmd{}
    55  	// nolint: dupl
    56  	cmd := &cobra.Command{
    57  		Use:     "build",
    58  		Aliases: []string{"b"},
    59  		Short:   "Builds the current project",
    60  		Long: `The ` + "`goreleaser build`" + ` command is analogous to the ` + "`go build`" + ` command, in the sense it only builds binaries.
    61  
    62  Its intended usage is, for example, within Makefiles to avoid setting up ldflags and etc in several places. That way, the GoReleaser config becomes the source of truth for how the binaries should be built.
    63  
    64  It also allows you to generate a local build for your current machine only using the ` + "`--single-target`" + ` option, and specific build IDs using the ` + "`--id`" + ` option in case you have more than one.
    65  
    66  When using ` + "`--single-target`" + `, the ` + "`GOOS`" + ` and ` + "`GOARCH`" + ` environment variables are used to determine the target, defaulting to the current machine target if not set.
    67  `,
    68  		SilenceUsage:      true,
    69  		SilenceErrors:     true,
    70  		Args:              cobra.NoArgs,
    71  		ValidArgsFunction: cobra.NoFileCompletions,
    72  		RunE: timedRunE("build", func(_ *cobra.Command, _ []string) error {
    73  			ctx, err := buildProject(root.opts)
    74  			if err != nil {
    75  				return err
    76  			}
    77  			deprecateWarn(ctx)
    78  			return nil
    79  		}),
    80  	}
    81  
    82  	cmd.Flags().StringVarP(&root.opts.config, "config", "f", "", "Load configuration from file")
    83  	_ = cmd.MarkFlagFilename("config", "yaml", "yml")
    84  	cmd.Flags().BoolVar(&root.opts.snapshot, "snapshot", false, "Generate an unversioned snapshot build, skipping all validations")
    85  	cmd.Flags().BoolVar(&root.opts.skipValidate, "skip-validate", false, "Skips several sanity checks")
    86  	cmd.Flags().BoolVar(&root.opts.skipBefore, "skip-before", false, "Skips global before hooks")
    87  	cmd.Flags().BoolVar(&root.opts.skipPostHooks, "skip-post-hooks", false, "Skips all post-build hooks")
    88  	cmd.Flags().BoolVar(&root.opts.clean, "clean", false, "Removes the 'dist' directory before building")
    89  	cmd.Flags().BoolVar(&root.opts.rmDist, "rm-dist", false, "Removes the 'dist' directory before building")
    90  	cmd.Flags().IntVarP(&root.opts.parallelism, "parallelism", "p", 0, "Amount tasks to run concurrently (default: number of CPUs)")
    91  	_ = cmd.RegisterFlagCompletionFunc("parallelism", cobra.NoFileCompletions)
    92  	cmd.Flags().DurationVar(&root.opts.timeout, "timeout", 30*time.Minute, "Timeout to the entire build process")
    93  	_ = cmd.RegisterFlagCompletionFunc("timeout", cobra.NoFileCompletions)
    94  	cmd.Flags().BoolVar(&root.opts.singleTarget, "single-target", false, "Builds only for current GOOS and GOARCH, regardless of what's set in the configuration file")
    95  	cmd.Flags().StringArrayVar(&root.opts.ids, "id", nil, "Builds only the specified build ids")
    96  	_ = cmd.RegisterFlagCompletionFunc("id", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
    97  		// TODO: improve this
    98  		cfg, err := loadConfig(root.opts.config)
    99  		if err != nil {
   100  			return nil, cobra.ShellCompDirectiveNoFileComp
   101  		}
   102  		ids := make([]string, 0, len(cfg.Builds))
   103  		for _, build := range cfg.Builds {
   104  			ids = append(ids, build.ID)
   105  		}
   106  		return ids, cobra.ShellCompDirectiveNoFileComp
   107  	})
   108  	cmd.Flags().BoolVar(&root.opts.deprecated, "deprecated", false, "Force print the deprecation message - tests only")
   109  	cmd.Flags().StringVarP(&root.opts.output, "output", "o", "", "Copy the binary to the path after the build. Only taken into account when using --single-target and a single id (either with --id or if configuration only has one build)")
   110  	_ = cmd.MarkFlagFilename("output", "")
   111  	_ = cmd.Flags().MarkHidden("rm-dist")
   112  	_ = cmd.Flags().MarkHidden("deprecated")
   113  
   114  	for _, f := range []string{
   115  		"post-hooks",
   116  		"before",
   117  		"validate",
   118  	} {
   119  		_ = cmd.Flags().MarkHidden("skip-" + f)
   120  		_ = cmd.Flags().MarkDeprecated("skip-"+f, fmt.Sprintf("please use --skip=%s instead", f))
   121  	}
   122  	cmd.Flags().StringSliceVar(
   123  		&root.opts.skips,
   124  		"skip",
   125  		nil,
   126  		fmt.Sprintf("Skip the given options (valid options are: %s)", skips.Build.String()),
   127  	)
   128  	_ = cmd.RegisterFlagCompletionFunc("skip", func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   129  		return skips.Build.Complete(toComplete), cobra.ShellCompDirectiveDefault
   130  	})
   131  
   132  	root.cmd = cmd
   133  	return root
   134  }
   135  
   136  func buildProject(options buildOpts) (*context.Context, error) {
   137  	cfg, err := loadConfig(options.config)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	ctx, cancel := context.NewWithTimeout(cfg, options.timeout)
   142  	defer cancel()
   143  	if err := setupBuildContext(ctx, options); err != nil {
   144  		return nil, err
   145  	}
   146  	return ctx, ctrlc.Default.Run(ctx, func() error {
   147  		for _, pipe := range setupPipeline(ctx, options) {
   148  			if err := skip.Maybe(
   149  				pipe,
   150  				logging.Log(
   151  					pipe.String(),
   152  					errhandler.Handle(pipe.Run),
   153  				),
   154  			)(ctx); err != nil {
   155  				return err
   156  			}
   157  		}
   158  		return nil
   159  	})
   160  }
   161  
   162  func setupPipeline(ctx *context.Context, options buildOpts) []pipeline.Piper {
   163  	if options.output != "" && options.singleTarget && (len(options.ids) > 0 || len(ctx.Config.Builds) == 1) {
   164  		return append(pipeline.BuildCmdPipeline, withOutputPipe{options.output})
   165  	}
   166  	return pipeline.BuildCmdPipeline
   167  }
   168  
   169  func setupBuildContext(ctx *context.Context, options buildOpts) error {
   170  	ctx.Action = context.ActionBuild
   171  	ctx.Deprecated = options.deprecated // test only
   172  	ctx.Parallelism = runtime.GOMAXPROCS(0)
   173  	if options.parallelism > 0 {
   174  		ctx.Parallelism = options.parallelism
   175  	}
   176  	log.Debugf("parallelism: %v", ctx.Parallelism)
   177  	ctx.Snapshot = options.snapshot
   178  
   179  	if err := skips.SetBuild(ctx, options.skips...); err != nil {
   180  		return err
   181  	}
   182  
   183  	if options.skipValidate {
   184  		skips.Set(ctx, skips.Validate)
   185  		deprecate.NoticeCustom(ctx, "-skip", "--skip-validate was deprecated in favor of --skip=validate, check {{ .URL }} for more details")
   186  	}
   187  	if options.skipBefore {
   188  		skips.Set(ctx, skips.Before)
   189  		deprecate.NoticeCustom(ctx, "-skip", "--skip-before was deprecated in favor of --skip=before, check {{ .URL }} for more details")
   190  	}
   191  	if options.skipPostHooks {
   192  		skips.Set(ctx, skips.PostBuildHooks)
   193  		deprecate.NoticeCustom(ctx, "-skip", "--skip-post-hooks was deprecated in favor of --skip=post-hooks, check {{ .URL }} for more details")
   194  	}
   195  
   196  	if options.rmDist {
   197  		deprecate.NoticeCustom(ctx, "-rm-dist", "--rm-dist was deprecated in favor of --clean, check {{ .URL }} for more details")
   198  	}
   199  
   200  	if ctx.Snapshot {
   201  		skips.Set(ctx, skips.Validate)
   202  	}
   203  
   204  	ctx.SkipTokenCheck = true
   205  	ctx.Clean = options.clean || options.rmDist
   206  
   207  	if options.singleTarget {
   208  		ctx.Partial = true
   209  	}
   210  
   211  	if len(options.ids) > 0 {
   212  		if err := setupBuildID(ctx, options.ids); err != nil {
   213  			return err
   214  		}
   215  	}
   216  
   217  	if skips.Any(ctx, skips.Build...) {
   218  		log.Warnf(
   219  			logext.Warning("skipping %s..."),
   220  			skips.String(ctx),
   221  		)
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  func setupBuildID(ctx *context.Context, ids []string) error {
   228  	if len(ctx.Config.Builds) < 2 {
   229  		log.Warn("single build in config, '--id' ignored")
   230  		return nil
   231  	}
   232  
   233  	var keep []config.Build
   234  	for _, build := range ctx.Config.Builds {
   235  		for _, id := range ids {
   236  			if build.ID == id {
   237  				keep = append(keep, build)
   238  				break
   239  			}
   240  		}
   241  	}
   242  
   243  	if len(keep) == 0 {
   244  		return fmt.Errorf("no builds with ids %s", strings.Join(ids, ", "))
   245  	}
   246  
   247  	ctx.Config.Builds = keep
   248  	return nil
   249  }
   250  
   251  // withOutputPipe copies the binary from dist to the specified output path.
   252  type withOutputPipe struct {
   253  	output string
   254  }
   255  
   256  func (w withOutputPipe) String() string {
   257  	return fmt.Sprintf("copying binary to %q", w.output)
   258  }
   259  
   260  func (w withOutputPipe) Run(ctx *context.Context) error {
   261  	bins := ctx.Artifacts.Filter(artifact.ByType(artifact.Binary)).List()
   262  	if len(bins) == 0 {
   263  		return fmt.Errorf("no binary found")
   264  	}
   265  	path := bins[0].Path
   266  	out := w.output
   267  	if out == "." {
   268  		out = filepath.Base(path)
   269  	}
   270  	return gio.Copy(path, out)
   271  }