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 }