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 }