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

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"runtime"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/caarlos0/ctrlc"
    12  	"github.com/caarlos0/log"
    13  	"github.com/spf13/cobra"
    14  	"github.com/windmeup/goreleaser/internal/artifact"
    15  	"github.com/windmeup/goreleaser/internal/deprecate"
    16  	"github.com/windmeup/goreleaser/internal/gio"
    17  	"github.com/windmeup/goreleaser/internal/logext"
    18  	"github.com/windmeup/goreleaser/internal/middleware/errhandler"
    19  	"github.com/windmeup/goreleaser/internal/middleware/logging"
    20  	"github.com/windmeup/goreleaser/internal/middleware/skip"
    21  	"github.com/windmeup/goreleaser/internal/pipeline"
    22  	"github.com/windmeup/goreleaser/internal/skips"
    23  	"github.com/windmeup/goreleaser/pkg/config"
    24  	"github.com/windmeup/goreleaser/pkg/context"
    25  	"golang.org/x/exp/slices"
    26  )
    27  
    28  type buildCmd struct {
    29  	cmd  *cobra.Command
    30  	opts buildOpts
    31  }
    32  
    33  type buildOpts struct {
    34  	config       string
    35  	ids          []string
    36  	snapshot     bool
    37  	clean        bool
    38  	deprecated   bool
    39  	parallelism  int
    40  	timeout      time.Duration
    41  	singleTarget bool
    42  	output       string
    43  	skips        []string
    44  
    45  	// Deprecated: use clean instead.
    46  	rmDist bool
    47  	// Deprecated: use skip instead.
    48  	skipValidate bool
    49  	// Deprecated: use skip instead.
    50  	skipBefore bool
    51  	// Deprecated: use skip instead.
    52  	skipPostHooks bool
    53  }
    54  
    55  func newBuildCmd() *buildCmd {
    56  	root := &buildCmd{}
    57  	// nolint: dupl
    58  	cmd := &cobra.Command{
    59  		Use:     "build",
    60  		Aliases: []string{"b"},
    61  		Short:   "Builds the current project",
    62  		Long: `The ` + "`goreleaser build`" + ` command is analogous to the ` + "`go build`" + ` command, in the sense it only builds binaries.
    63  
    64  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.
    65  
    66  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.
    67  
    68  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.
    69  `,
    70  		SilenceUsage:      true,
    71  		SilenceErrors:     true,
    72  		Args:              cobra.NoArgs,
    73  		ValidArgsFunction: cobra.NoFileCompletions,
    74  		RunE: timedRunE("build", func(cmd *cobra.Command, args []string) error {
    75  			ctx, err := buildProject(root.opts)
    76  			if err != nil {
    77  				return err
    78  			}
    79  			deprecateWarn(ctx)
    80  			return nil
    81  		}),
    82  	}
    83  
    84  	cmd.Flags().StringVarP(&root.opts.config, "config", "f", "", "Load configuration from file")
    85  	_ = cmd.MarkFlagFilename("config", "yaml", "yml")
    86  	cmd.Flags().BoolVar(&root.opts.snapshot, "snapshot", false, "Generate an unversioned snapshot build, skipping all validations")
    87  	cmd.Flags().BoolVar(&root.opts.skipValidate, "skip-validate", false, "Skips several sanity checks")
    88  	cmd.Flags().BoolVar(&root.opts.skipBefore, "skip-before", false, "Skips global before hooks")
    89  	cmd.Flags().BoolVar(&root.opts.skipPostHooks, "skip-post-hooks", false, "Skips all post-build hooks")
    90  	cmd.Flags().BoolVar(&root.opts.clean, "clean", false, "Remove the dist folder before building")
    91  	cmd.Flags().BoolVar(&root.opts.rmDist, "rm-dist", false, "Remove the dist folder before building")
    92  	cmd.Flags().IntVarP(&root.opts.parallelism, "parallelism", "p", 0, "Amount tasks to run concurrently (default: number of CPUs)")
    93  	_ = cmd.RegisterFlagCompletionFunc("parallelism", cobra.NoFileCompletions)
    94  	cmd.Flags().DurationVar(&root.opts.timeout, "timeout", 30*time.Minute, "Timeout to the entire build process")
    95  	_ = cmd.RegisterFlagCompletionFunc("timeout", cobra.NoFileCompletions)
    96  	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")
    97  	cmd.Flags().StringArrayVar(&root.opts.ids, "id", nil, "Builds only the specified build ids")
    98  	_ = cmd.RegisterFlagCompletionFunc("id", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
    99  		// TODO: improve this
   100  		cfg, err := loadConfig(root.opts.config)
   101  		if err != nil {
   102  			return nil, cobra.ShellCompDirectiveNoFileComp
   103  		}
   104  		ids := make([]string, 0, len(cfg.Builds))
   105  		for _, build := range cfg.Builds {
   106  			ids = append(ids, build.ID)
   107  		}
   108  		return ids, cobra.ShellCompDirectiveNoFileComp
   109  	})
   110  	cmd.Flags().BoolVar(&root.opts.deprecated, "deprecated", false, "Force print the deprecation message - tests only")
   111  	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)")
   112  	_ = cmd.MarkFlagFilename("output", "")
   113  	_ = cmd.Flags().MarkHidden("rm-dist")
   114  	_ = cmd.Flags().MarkHidden("deprecated")
   115  
   116  	for _, f := range []string{
   117  		"post-hooks",
   118  		"before",
   119  		"validate",
   120  	} {
   121  		_ = cmd.Flags().MarkHidden("skip-" + f)
   122  		_ = cmd.Flags().MarkDeprecated("skip-"+f, fmt.Sprintf("please use --skip=%s instead", f))
   123  	}
   124  	cmd.Flags().StringSliceVar(
   125  		&root.opts.skips,
   126  		"skip",
   127  		nil,
   128  		fmt.Sprintf("Skip the given options (valid options are %s)", skips.Build.String()),
   129  	)
   130  	_ = cmd.RegisterFlagCompletionFunc("skip", func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   131  		return skips.Build.Complete(toComplete), cobra.ShellCompDirectiveDefault
   132  	})
   133  
   134  	root.cmd = cmd
   135  	return root
   136  }
   137  
   138  func buildProject(options buildOpts) (*context.Context, error) {
   139  	cfg, err := loadConfig(options.config)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	ctx, cancel := context.NewWithTimeout(cfg, options.timeout)
   144  	defer cancel()
   145  	if err := setupBuildContext(ctx, options); err != nil {
   146  		return nil, err
   147  	}
   148  	return ctx, ctrlc.Default.Run(ctx, func() error {
   149  		for _, pipe := range setupPipeline(ctx, options) {
   150  			if err := skip.Maybe(
   151  				pipe,
   152  				logging.Log(
   153  					pipe.String(),
   154  					errhandler.Handle(pipe.Run),
   155  				),
   156  			)(ctx); err != nil {
   157  				return err
   158  			}
   159  		}
   160  		return nil
   161  	})
   162  }
   163  
   164  func setupPipeline(ctx *context.Context, options buildOpts) []pipeline.Piper {
   165  	if options.output != "" && options.singleTarget && (len(options.ids) > 0 || len(ctx.Config.Builds) == 1) {
   166  		return append(pipeline.BuildCmdPipeline, withOutputPipe{options.output})
   167  	}
   168  	return pipeline.BuildCmdPipeline
   169  }
   170  
   171  func setupBuildContext(ctx *context.Context, options buildOpts) error {
   172  	ctx.Deprecated = options.deprecated // test only
   173  	ctx.Parallelism = runtime.GOMAXPROCS(0)
   174  	if options.parallelism > 0 {
   175  		ctx.Parallelism = options.parallelism
   176  	}
   177  	log.Debugf("parallelism: %v", ctx.Parallelism)
   178  	ctx.Snapshot = options.snapshot
   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  		if err := setupBuildSingleTarget(ctx); err != nil {
   209  			return err
   210  		}
   211  	}
   212  
   213  	if len(options.ids) > 0 {
   214  		if err := setupBuildID(ctx, options.ids); err != nil {
   215  			return err
   216  		}
   217  	}
   218  
   219  	if skips.Any(ctx, skips.Build...) {
   220  		log.Warnf(
   221  			logext.Warning("skipping %s..."),
   222  			skips.String(ctx),
   223  		)
   224  	}
   225  
   226  	return nil
   227  }
   228  
   229  func setupBuildSingleTarget(ctx *context.Context) error {
   230  	goos := os.Getenv("GOOS")
   231  	if goos == "" {
   232  		goos = runtime.GOOS
   233  	}
   234  	goarch := os.Getenv("GOARCH")
   235  	if goarch == "" {
   236  		goarch = runtime.GOARCH
   237  	}
   238  	log.WithField("reason", "single target is enabled").Warnf("building only for %s/%s", goos, goarch)
   239  	if len(ctx.Config.Builds) == 0 {
   240  		ctx.Config.Builds = append(ctx.Config.Builds, config.Build{})
   241  	}
   242  	var keep []config.Build
   243  	for _, build := range ctx.Config.Builds {
   244  		if !shouldBuild(build, goos, goarch) {
   245  			continue
   246  		}
   247  		build.Goos = []string{goos}
   248  		build.Goarch = []string{goarch}
   249  		build.Goarm = nil
   250  		build.Gomips = nil
   251  		build.Goamd64 = nil
   252  		build.Targets = nil
   253  		keep = append(keep, build)
   254  	}
   255  
   256  	ctx.Config.Builds = keep
   257  	ctx.Config.UniversalBinaries = nil
   258  
   259  	if len(keep) == 0 {
   260  		return fmt.Errorf("no builds matching --single-target %s/%s", goos, goarch)
   261  	}
   262  
   263  	return nil
   264  }
   265  
   266  func shouldBuild(build config.Build, goos, goarch string) bool {
   267  	if len(build.Targets) > 0 {
   268  		return slices.ContainsFunc(build.Targets, func(e string) bool {
   269  			return strings.HasPrefix(e, fmt.Sprintf("%s_%s", goos, goarch))
   270  		})
   271  	}
   272  	return (len(build.Goos) == 0 && len(build.Goarch) == 0) ||
   273  		(slices.Contains(build.Goos, goos) &&
   274  			slices.Contains(build.Goarch, goarch))
   275  }
   276  
   277  func setupBuildID(ctx *context.Context, ids []string) error {
   278  	if len(ctx.Config.Builds) < 2 {
   279  		log.Warn("single build in config, '--id' ignored")
   280  		return nil
   281  	}
   282  
   283  	var keep []config.Build
   284  	for _, build := range ctx.Config.Builds {
   285  		for _, id := range ids {
   286  			if build.ID == id {
   287  				keep = append(keep, build)
   288  				break
   289  			}
   290  		}
   291  	}
   292  
   293  	if len(keep) == 0 {
   294  		return fmt.Errorf("no builds with ids %s", strings.Join(ids, ", "))
   295  	}
   296  
   297  	ctx.Config.Builds = keep
   298  	return nil
   299  }
   300  
   301  // withOutputPipe copies the binary from dist to the specified output path.
   302  type withOutputPipe struct {
   303  	output string
   304  }
   305  
   306  func (w withOutputPipe) String() string {
   307  	return fmt.Sprintf("copying binary to %q", w.output)
   308  }
   309  
   310  func (w withOutputPipe) Run(ctx *context.Context) error {
   311  	bins := ctx.Artifacts.Filter(artifact.ByType(artifact.Binary)).List()
   312  	if len(bins) == 0 {
   313  		return fmt.Errorf("no binary found")
   314  	}
   315  	path := bins[0].Path
   316  	out := w.output
   317  	if out == "." {
   318  		out = filepath.Base(path)
   319  	}
   320  	return gio.Copy(path, out)
   321  }