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