github.com/triarius/goreleaser@v1.12.5/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 "strconv" 12 "strings" 13 "time" 14 15 "github.com/caarlos0/log" 16 "github.com/triarius/goreleaser/internal/artifact" 17 "github.com/triarius/goreleaser/internal/builders/buildtarget" 18 "github.com/triarius/goreleaser/internal/tmpl" 19 api "github.com/triarius/goreleaser/pkg/build" 20 "github.com/triarius/goreleaser/pkg/config" 21 "github.com/triarius/goreleaser/pkg/context" 22 "github.com/imdario/mergo" 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 if len(build.Targets) == 0 { 55 if len(build.Goos) == 0 { 56 build.Goos = []string{"linux", "darwin"} 57 } 58 if len(build.Goarch) == 0 { 59 build.Goarch = []string{"amd64", "arm64", "386"} 60 } 61 if len(build.Goarm) == 0 { 62 build.Goarm = []string{"6"} 63 } 64 if len(build.Gomips) == 0 { 65 build.Gomips = []string{"hardfloat"} 66 } 67 if len(build.Goamd64) == 0 { 68 build.Goamd64 = []string{"v1"} 69 } 70 targets, err := buildtarget.List(build) 71 if err != nil { 72 return build, err 73 } 74 build.Targets = targets 75 } else { 76 targets := map[string]bool{} 77 for _, target := range build.Targets { 78 if target == go118FirstClassTargetsName || 79 target == goStableFirstClassTargetsName { 80 for _, t := range go118FirstClassTargets { 81 targets[t] = true 82 } 83 continue 84 } 85 if strings.HasSuffix(target, "_amd64") { 86 targets[target+"_v1"] = true 87 continue 88 } 89 if strings.HasSuffix(target, "_arm") { 90 targets[target+"_6"] = true 91 continue 92 } 93 if strings.HasSuffix(target, "_mips") || 94 strings.HasSuffix(target, "_mips64") || 95 strings.HasSuffix(target, "_mipsle") || 96 strings.HasSuffix(target, "_mips64le") { 97 targets[target+"_hardfloat"] = true 98 continue 99 } 100 targets[target] = true 101 } 102 build.Targets = keys(targets) 103 } 104 return build, nil 105 } 106 107 func keys(m map[string]bool) []string { 108 result := make([]string, 0, len(m)) 109 for k := range m { 110 result = append(result, k) 111 } 112 return result 113 } 114 115 const ( 116 go118FirstClassTargetsName = "go_118_first_class" 117 goStableFirstClassTargetsName = "go_first_class" 118 ) 119 120 // go tool dist list -json | jq -r '.[] | select(.FirstClass) | [.GOOS, .GOARCH] | @tsv' 121 var go118FirstClassTargets = []string{ 122 "darwin_amd64_v1", 123 "darwin_arm64", 124 "linux_386", 125 "linux_amd64_v1", 126 "linux_arm_6", 127 "linux_arm64", 128 "windows_386", 129 "windows_amd64_v1", 130 } 131 132 // Build builds a golang build. 133 func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error { 134 if err := checkMain(build); err != nil { 135 return err 136 } 137 138 artifact := &artifact.Artifact{ 139 Type: artifact.Binary, 140 Path: options.Path, 141 Name: options.Name, 142 Goos: options.Goos, 143 Goarch: options.Goarch, 144 Goamd64: options.Goamd64, 145 Goarm: options.Goarm, 146 Gomips: options.Gomips, 147 Extra: map[string]interface{}{ 148 artifact.ExtraBinary: strings.TrimSuffix(filepath.Base(options.Path), options.Ext), 149 artifact.ExtraExt: options.Ext, 150 artifact.ExtraID: build.ID, 151 }, 152 } 153 154 details, err := withOverrides(ctx, build, options) 155 if err != nil { 156 return err 157 } 158 159 env := append(ctx.Env.Strings(), details.Env...) 160 env = append( 161 env, 162 "GOOS="+options.Goos, 163 "GOARCH="+options.Goarch, 164 "GOARM="+options.Goarm, 165 "GOMIPS="+options.Gomips, 166 "GOMIPS64="+options.Gomips, 167 "GOAMD64="+options.Goamd64, 168 ) 169 170 cmd, err := buildGoBuildLine(ctx, build, details, options, artifact, env) 171 if err != nil { 172 return err 173 } 174 175 if err := run(ctx, cmd, env, build.Dir); err != nil { 176 return fmt.Errorf("failed to build for %s: %w", options.Target, err) 177 } 178 179 if build.ModTimestamp != "" { 180 modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(artifact, map[string]string{}).Apply(build.ModTimestamp) 181 if err != nil { 182 return err 183 } 184 modUnix, err := strconv.ParseInt(modTimestamp, 10, 64) 185 if err != nil { 186 return err 187 } 188 modTime := time.Unix(modUnix, 0) 189 err = os.Chtimes(options.Path, modTime, modTime) 190 if err != nil { 191 return fmt.Errorf("failed to change times for %s: %w", options.Target, err) 192 } 193 } 194 195 ctx.Artifacts.Add(artifact) 196 return nil 197 } 198 199 func withOverrides(ctx *context.Context, build config.Build, options api.Options) (config.BuildDetails, error) { 200 optsTarget := options.Goos + options.Goarch + options.Goarm + options.Gomips + options.Goamd64 201 for _, o := range build.BuildDetailsOverrides { 202 overrideTarget, err := tmpl.New(ctx).Apply(o.Goos + o.Goarch + o.Gomips + o.Goarm + o.Goamd64) 203 if err != nil { 204 return build.BuildDetails, err 205 } 206 207 if optsTarget == overrideTarget { 208 dets := config.BuildDetails{ 209 Ldflags: build.BuildDetails.Ldflags, 210 Tags: build.BuildDetails.Tags, 211 Flags: build.BuildDetails.Flags, 212 Asmflags: build.BuildDetails.Asmflags, 213 Gcflags: build.BuildDetails.Gcflags, 214 } 215 if err := mergo.Merge(&dets, o.BuildDetails, mergo.WithOverride); err != nil { 216 return build.BuildDetails, err 217 } 218 219 dets.Env = context.ToEnv(append(build.Env, o.BuildDetails.Env...)).Strings() 220 log.WithField("details", dets).Infof("overridden build details for %s", optsTarget) 221 return dets, nil 222 } 223 } 224 225 return build.BuildDetails, nil 226 } 227 228 func buildGoBuildLine(ctx *context.Context, build config.Build, details config.BuildDetails, options api.Options, artifact *artifact.Artifact, env []string) ([]string, error) { 229 cmd := []string{build.GoBinary, build.Command} 230 231 flags, err := processFlags(ctx, artifact, env, details.Flags, "") 232 if err != nil { 233 return cmd, err 234 } 235 cmd = append(cmd, flags...) 236 237 asmflags, err := processFlags(ctx, artifact, env, details.Asmflags, "-asmflags=") 238 if err != nil { 239 return cmd, err 240 } 241 cmd = append(cmd, asmflags...) 242 243 gcflags, err := processFlags(ctx, artifact, env, details.Gcflags, "-gcflags=") 244 if err != nil { 245 return cmd, err 246 } 247 cmd = append(cmd, gcflags...) 248 249 // tags is not a repeatable flag 250 if len(details.Tags) > 0 { 251 tags, err := processFlags(ctx, artifact, env, details.Tags, "") 252 if err != nil { 253 return cmd, err 254 } 255 cmd = append(cmd, "-tags="+strings.Join(tags, ",")) 256 } 257 258 // ldflags is not a repeatable flag 259 if len(details.Ldflags) > 0 { 260 // flag prefix is skipped because ldflags need to output a single string 261 ldflags, err := processFlags(ctx, artifact, env, details.Ldflags, "") 262 if err != nil { 263 return cmd, err 264 } 265 // ldflags need to be single string in order to apply correctly 266 cmd = append(cmd, "-ldflags="+strings.Join(ldflags, " ")) 267 } 268 269 cmd = append(cmd, "-o", options.Path, build.Main) 270 return cmd, nil 271 } 272 273 func processFlags(ctx *context.Context, a *artifact.Artifact, env, flags []string, flagPrefix string) ([]string, error) { 274 processed := make([]string, 0, len(flags)) 275 for _, rawFlag := range flags { 276 flag, err := processFlag(ctx, a, env, rawFlag) 277 if err != nil { 278 return nil, err 279 } 280 processed = append(processed, flagPrefix+flag) 281 } 282 return processed, nil 283 } 284 285 func processFlag(ctx *context.Context, a *artifact.Artifact, env []string, rawFlag string) (string, error) { 286 return tmpl.New(ctx).WithEnvS(env).WithArtifact(a, map[string]string{}).Apply(rawFlag) 287 } 288 289 func run(ctx *context.Context, command, env []string, dir string) error { 290 /* #nosec */ 291 cmd := exec.CommandContext(ctx, command[0], command[1:]...) 292 log := log.WithField("env", env).WithField("cmd", command) 293 cmd.Env = env 294 cmd.Dir = dir 295 log.Debug("running") 296 if out, err := cmd.CombinedOutput(); err != nil { 297 return fmt.Errorf("%w: %s", err, string(out)) 298 } 299 return nil 300 } 301 302 func checkMain(build config.Build) error { 303 if build.NoMainCheck { 304 return nil 305 } 306 main := build.Main 307 if build.UnproxiedMain != "" { 308 main = build.UnproxiedMain 309 } 310 dir := build.Dir 311 if build.UnproxiedDir != "" { 312 dir = build.UnproxiedDir 313 } 314 315 if main == "" { 316 main = "." 317 } 318 if dir != "" { 319 main = filepath.Join(dir, main) 320 } 321 stat, ferr := os.Stat(main) 322 if ferr != nil { 323 return fmt.Errorf("couldn't find main file: %w", ferr) 324 } 325 if stat.IsDir() { 326 packs, err := parser.ParseDir(token.NewFileSet(), main, nil, 0) 327 if err != nil { 328 return fmt.Errorf("failed to parse dir: %s: %w", main, err) 329 } 330 for _, pack := range packs { 331 for _, file := range pack.Files { 332 if hasMain(file) { 333 return nil 334 } 335 } 336 } 337 return errNoMain{build.Binary} 338 } 339 file, err := parser.ParseFile(token.NewFileSet(), main, nil, 0) 340 if err != nil { 341 return fmt.Errorf("failed to parse file: %s: %w", main, err) 342 } 343 if hasMain(file) { 344 return nil 345 } 346 return errNoMain{build.Binary} 347 } 348 349 type errNoMain struct { 350 bin string 351 } 352 353 func (e errNoMain) Error() string { 354 return fmt.Sprintf("build for %s does not contain a main function\nLearn more at https://goreleaser.com/errors/no-main\n", e.bin) 355 } 356 357 func hasMain(file *ast.File) bool { 358 for _, decl := range file.Decls { 359 fn, isFn := decl.(*ast.FuncDecl) 360 if !isFn { 361 continue 362 } 363 if fn.Name.Name == "main" && fn.Recv == nil { 364 return true 365 } 366 } 367 return false 368 }