github.com/goreleaser/goreleaser@v1.25.1/internal/builders/golang/build.go (about) 1 package golang 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/parser" 7 "go/token" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "slices" 12 "strings" 13 14 "dario.cat/mergo" 15 "github.com/caarlos0/log" 16 "github.com/goreleaser/goreleaser/internal/artifact" 17 "github.com/goreleaser/goreleaser/internal/builders/buildtarget" 18 "github.com/goreleaser/goreleaser/internal/gio" 19 "github.com/goreleaser/goreleaser/internal/logext" 20 "github.com/goreleaser/goreleaser/internal/tmpl" 21 api "github.com/goreleaser/goreleaser/pkg/build" 22 "github.com/goreleaser/goreleaser/pkg/config" 23 "github.com/goreleaser/goreleaser/pkg/context" 24 ) 25 26 // Default builder instance. 27 // nolint: gochecknoglobals 28 var Default = &Builder{} 29 30 // nolint: gochecknoinits 31 func init() { 32 api.Register("go", Default) 33 } 34 35 // Builder is golang builder. 36 type Builder struct{} 37 38 // WithDefaults sets the defaults for a golang build and returns it. 39 func (*Builder) WithDefaults(build config.Build) (config.Build, error) { 40 if build.GoBinary == "" { 41 build.GoBinary = "go" 42 } 43 if build.Command == "" { 44 build.Command = "build" 45 } 46 if build.Dir == "" { 47 build.Dir = "." 48 } 49 if build.Main == "" { 50 build.Main = "." 51 } 52 if len(build.Ldflags) == 0 { 53 build.Ldflags = []string{"-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser"} 54 } 55 56 _ = warnIfTargetsAndOtherOptionTogether(build) 57 if len(build.Targets) == 0 { 58 if len(build.Goos) == 0 { 59 build.Goos = []string{"linux", "darwin", "windows"} 60 } 61 if len(build.Goarch) == 0 { 62 build.Goarch = []string{"amd64", "arm64", "386"} 63 } 64 if len(build.Goarm) == 0 { 65 build.Goarm = []string{"6"} 66 } 67 if len(build.Gomips) == 0 { 68 build.Gomips = []string{"hardfloat"} 69 } 70 if len(build.Goamd64) == 0 { 71 build.Goamd64 = []string{"v1"} 72 } 73 targets, err := buildtarget.List(build) 74 if err != nil { 75 return build, err 76 } 77 build.Targets = targets 78 } else { 79 targets := map[string]bool{} 80 for _, target := range build.Targets { 81 if target == go118FirstClassTargetsName || 82 target == goStableFirstClassTargetsName { 83 for _, t := range go118FirstClassTargets { 84 targets[t] = true 85 } 86 continue 87 } 88 if strings.HasSuffix(target, "_amd64") { 89 targets[target+"_v1"] = true 90 continue 91 } 92 if strings.HasSuffix(target, "_arm") { 93 targets[target+"_6"] = true 94 continue 95 } 96 if strings.HasSuffix(target, "_mips") || 97 strings.HasSuffix(target, "_mips64") || 98 strings.HasSuffix(target, "_mipsle") || 99 strings.HasSuffix(target, "_mips64le") { 100 targets[target+"_hardfloat"] = true 101 continue 102 } 103 targets[target] = true 104 } 105 build.Targets = keys(targets) 106 } 107 return build, nil 108 } 109 110 func warnIfTargetsAndOtherOptionTogether(build config.Build) bool { 111 if len(build.Targets) == 0 { 112 return false 113 } 114 115 res := false 116 for k, v := range map[string]int{ 117 "goos": len(build.Goos), 118 "goarch": len(build.Goarch), 119 "goarm": len(build.Goarm), 120 "gomips": len(build.Gomips), 121 "goamd64": len(build.Goamd64), 122 "ignore": len(build.Ignore), 123 } { 124 if v == 0 { 125 continue 126 } 127 log.Warnf(logext.Keyword("builds."+k) + " is ignored when " + logext.Keyword("builds.targets") + " is set") 128 res = true 129 } 130 return res 131 } 132 133 func keys(m map[string]bool) []string { 134 result := make([]string, 0, len(m)) 135 for k := range m { 136 result = append(result, k) 137 } 138 return result 139 } 140 141 const ( 142 go118FirstClassTargetsName = "go_118_first_class" 143 goStableFirstClassTargetsName = "go_first_class" 144 ) 145 146 // go tool dist list -json | jq -r '.[] | select(.FirstClass) | [.GOOS, .GOARCH] | @tsv' 147 var go118FirstClassTargets = []string{ 148 "darwin_amd64_v1", 149 "darwin_arm64", 150 "linux_386", 151 "linux_amd64_v1", 152 "linux_arm_6", 153 "linux_arm64", 154 "windows_386", 155 "windows_amd64_v1", 156 } 157 158 // Build builds a golang build. 159 func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error { 160 if err := checkMain(build); err != nil { 161 return err 162 } 163 164 a := &artifact.Artifact{ 165 Type: artifact.Binary, 166 Path: options.Path, 167 Name: options.Name, 168 Goos: options.Goos, 169 Goarch: options.Goarch, 170 Goamd64: options.Goamd64, 171 Goarm: options.Goarm, 172 Gomips: options.Gomips, 173 Extra: map[string]interface{}{ 174 artifact.ExtraBinary: strings.TrimSuffix(filepath.Base(options.Path), options.Ext), 175 artifact.ExtraExt: options.Ext, 176 artifact.ExtraID: build.ID, 177 }, 178 } 179 180 if build.Buildmode == "c-archive" { 181 a.Type = artifact.CArchive 182 ctx.Artifacts.Add(getHeaderArtifactForLibrary(build, options)) 183 } 184 if build.Buildmode == "c-shared" { 185 a.Type = artifact.CShared 186 ctx.Artifacts.Add(getHeaderArtifactForLibrary(build, options)) 187 } 188 189 details, err := withOverrides(ctx, build, options) 190 if err != nil { 191 return err 192 } 193 194 env := []string{} 195 // used for unit testing only 196 testEnvs := []string{} 197 env = append(env, ctx.Env.Strings()...) 198 for _, e := range details.Env { 199 ee, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(a).Apply(e) 200 if err != nil { 201 return err 202 } 203 log.Debugf("env %q evaluated to %q", e, ee) 204 if ee != "" { 205 env = append(env, ee) 206 if strings.HasPrefix(e, "TEST_") { 207 testEnvs = append(testEnvs, ee) 208 } 209 } 210 } 211 env = append( 212 env, 213 "GOOS="+options.Goos, 214 "GOARCH="+options.Goarch, 215 "GOARM="+options.Goarm, 216 "GOMIPS="+options.Gomips, 217 "GOMIPS64="+options.Gomips, 218 "GOAMD64="+options.Goamd64, 219 ) 220 221 if len(testEnvs) > 0 { 222 a.Extra["testEnvs"] = testEnvs 223 } 224 225 cmd, err := buildGoBuildLine(ctx, build, details, options, a, env) 226 if err != nil { 227 return err 228 } 229 230 if err := run(ctx, cmd, env, build.Dir); err != nil { 231 return fmt.Errorf("failed to build for %s: %w", options.Target, err) 232 } 233 234 modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(a).Apply(build.ModTimestamp) 235 if err != nil { 236 return err 237 } 238 if err := gio.Chtimes(options.Path, modTimestamp); err != nil { 239 return err 240 } 241 242 ctx.Artifacts.Add(a) 243 return nil 244 } 245 246 func withOverrides(ctx *context.Context, build config.Build, options api.Options) (config.BuildDetails, error) { 247 optsTarget := options.Goos + options.Goarch + options.Goarm + options.Gomips + options.Goamd64 248 for _, o := range build.BuildDetailsOverrides { 249 overrideTarget, err := tmpl.New(ctx).Apply(o.Goos + o.Goarch + o.Gomips + o.Goarm + o.Goamd64) 250 if err != nil { 251 return build.BuildDetails, err 252 } 253 254 if optsTarget == overrideTarget { 255 dets := config.BuildDetails{ 256 Buildmode: build.BuildDetails.Buildmode, 257 Ldflags: build.BuildDetails.Ldflags, 258 Tags: build.BuildDetails.Tags, 259 Flags: build.BuildDetails.Flags, 260 Asmflags: build.BuildDetails.Asmflags, 261 Gcflags: build.BuildDetails.Gcflags, 262 } 263 if err := mergo.Merge(&dets, o.BuildDetails, mergo.WithOverride); err != nil { 264 return build.BuildDetails, err 265 } 266 267 dets.Env = context.ToEnv(append(build.Env, o.BuildDetails.Env...)).Strings() 268 log.WithField("details", dets).Infof("overridden build details for %s", optsTarget) 269 return dets, nil 270 } 271 } 272 273 return build.BuildDetails, nil 274 } 275 276 func buildGoBuildLine( 277 ctx *context.Context, 278 build config.Build, 279 details config.BuildDetails, 280 options api.Options, 281 artifact *artifact.Artifact, 282 env []string, 283 ) ([]string, error) { 284 gobin, err := tmpl.New(ctx).WithBuildOptions(options).Apply(build.GoBinary) 285 if err != nil { 286 return nil, err 287 } 288 cmd := []string{gobin, build.Command} 289 290 // tags, ldflags, and buildmode, should only appear once, warning only to avoid a breaking change 291 validateUniqueFlags(details) 292 293 flags, err := processFlags(ctx, artifact, env, details.Flags, "") 294 if err != nil { 295 return cmd, err 296 } 297 cmd = append(cmd, flags...) 298 if build.Command == "test" && !slices.Contains(flags, "-c") { 299 cmd = append(cmd, "-c") 300 } 301 302 asmflags, err := processFlags(ctx, artifact, env, details.Asmflags, "-asmflags=") 303 if err != nil { 304 return cmd, err 305 } 306 cmd = append(cmd, asmflags...) 307 308 gcflags, err := processFlags(ctx, artifact, env, details.Gcflags, "-gcflags=") 309 if err != nil { 310 return cmd, err 311 } 312 cmd = append(cmd, gcflags...) 313 314 // tags is not a repeatable flag 315 if len(details.Tags) > 0 { 316 tags, err := processFlags(ctx, artifact, env, details.Tags, "") 317 if err != nil { 318 return cmd, err 319 } 320 cmd = append(cmd, "-tags="+strings.Join(tags, ",")) 321 } 322 323 // ldflags is not a repeatable flag 324 if len(details.Ldflags) > 0 { 325 // flag prefix is skipped because ldflags need to output a single string 326 ldflags, err := processFlags(ctx, artifact, env, details.Ldflags, "") 327 if err != nil { 328 return cmd, err 329 } 330 // ldflags need to be single string in order to apply correctly 331 cmd = append(cmd, "-ldflags="+strings.Join(ldflags, " ")) 332 } 333 334 if details.Buildmode != "" { 335 cmd = append(cmd, "-buildmode="+details.Buildmode) 336 } 337 338 cmd = append(cmd, "-o", options.Path, build.Main) 339 return cmd, nil 340 } 341 342 func validateUniqueFlags(details config.BuildDetails) { 343 for _, flag := range details.Flags { 344 if strings.HasPrefix(flag, "-tags") && len(details.Tags) > 0 { 345 log.WithField("flag", flag).WithField("tags", details.Tags).Warn("tags is defined twice") 346 } 347 if strings.HasPrefix(flag, "-ldflags") && len(details.Ldflags) > 0 { 348 log.WithField("flag", flag).WithField("ldflags", details.Ldflags).Warn("ldflags is defined twice") 349 } 350 if strings.HasPrefix(flag, "-buildmode") && details.Buildmode != "" { 351 log.WithField("flag", flag).WithField("buildmode", details.Buildmode).Warn("buildmode is defined twice") 352 } 353 } 354 } 355 356 func processFlags(ctx *context.Context, a *artifact.Artifact, env, flags []string, flagPrefix string) ([]string, error) { 357 processed := make([]string, 0, len(flags)) 358 for _, rawFlag := range flags { 359 flag, err := processFlag(ctx, a, env, rawFlag) 360 if err != nil { 361 return nil, err 362 } 363 processed = append(processed, flagPrefix+flag) 364 } 365 return processed, nil 366 } 367 368 func processFlag(ctx *context.Context, a *artifact.Artifact, env []string, rawFlag string) (string, error) { 369 return tmpl.New(ctx).WithEnvS(env).WithArtifact(a).Apply(rawFlag) 370 } 371 372 func run(ctx *context.Context, command, env []string, dir string) error { 373 /* #nosec */ 374 cmd := exec.CommandContext(ctx, command[0], command[1:]...) 375 log := log.WithField("env", env).WithField("cmd", command) 376 cmd.Env = env 377 cmd.Dir = dir 378 log.Debug("running") 379 out, err := cmd.CombinedOutput() 380 if err != nil { 381 return fmt.Errorf("%w: %s", err, string(out)) 382 } 383 log.Debug(string(out)) 384 return nil 385 } 386 387 func checkMain(build config.Build) error { 388 if build.NoMainCheck { 389 return nil 390 } 391 main := build.Main 392 if build.UnproxiedMain != "" { 393 main = build.UnproxiedMain 394 } 395 dir := build.Dir 396 if build.UnproxiedDir != "" { 397 dir = build.UnproxiedDir 398 } 399 400 if main == "" { 401 main = "." 402 } 403 if dir != "" { 404 main = filepath.Join(dir, main) 405 } 406 stat, ferr := os.Stat(main) 407 if ferr != nil { 408 return fmt.Errorf("couldn't find main file: %w", ferr) 409 } 410 if stat.IsDir() { 411 packs, err := parser.ParseDir(token.NewFileSet(), main, nil, 0) 412 if err != nil { 413 return fmt.Errorf("failed to parse dir: %s: %w", main, err) 414 } 415 for _, pack := range packs { 416 for _, file := range pack.Files { 417 if hasMain(file) { 418 return nil 419 } 420 } 421 } 422 return errNoMain{build.Binary} 423 } 424 file, err := parser.ParseFile(token.NewFileSet(), main, nil, 0) 425 if err != nil { 426 return fmt.Errorf("failed to parse file: %s: %w", main, err) 427 } 428 if hasMain(file) { 429 return nil 430 } 431 return errNoMain{build.Binary} 432 } 433 434 type errNoMain struct { 435 bin string 436 } 437 438 func (e errNoMain) Error() string { 439 return fmt.Sprintf("build for %s does not contain a main function\nLearn more at https://goreleaser.com/errors/no-main\n", e.bin) 440 } 441 442 func hasMain(file *ast.File) bool { 443 for _, decl := range file.Decls { 444 fn, isFn := decl.(*ast.FuncDecl) 445 if !isFn { 446 continue 447 } 448 if fn.Name.Name == "main" && fn.Recv == nil { 449 return true 450 } 451 } 452 return false 453 } 454 455 func getHeaderArtifactForLibrary(build config.Build, options api.Options) *artifact.Artifact { 456 fullPathWithoutExt := strings.TrimSuffix(options.Path, options.Ext) 457 basePath := filepath.Base(fullPathWithoutExt) 458 fullPath := fullPathWithoutExt + ".h" 459 headerName := basePath + ".h" 460 461 return &artifact.Artifact{ 462 Type: artifact.Header, 463 Path: fullPath, 464 Name: headerName, 465 Goos: options.Goos, 466 Goarch: options.Goarch, 467 Goamd64: options.Goamd64, 468 Goarm: options.Goarm, 469 Gomips: options.Gomips, 470 Extra: map[string]interface{}{ 471 artifact.ExtraBinary: headerName, 472 artifact.ExtraExt: ".h", 473 artifact.ExtraID: build.ID, 474 }, 475 } 476 }