github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/internal/builders/golang/build.go (about) 1 package golang 2 3 import ( 4 "errors" 5 "fmt" 6 "go/ast" 7 "go/parser" 8 "go/token" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/apex/log" 17 "github.com/goreleaser/goreleaser/internal/artifact" 18 "github.com/goreleaser/goreleaser/internal/tmpl" 19 api "github.com/goreleaser/goreleaser/pkg/build" 20 "github.com/goreleaser/goreleaser/pkg/config" 21 "github.com/goreleaser/goreleaser/pkg/context" 22 ) 23 24 // Default builder instance. 25 // nolint: gochecknoglobals 26 var Default = &Builder{} 27 28 // nolint: gochecknoinits 29 func init() { 30 api.Register("go", Default) 31 } 32 33 // Builder is golang builder. 34 type Builder struct{} 35 36 // WithDefaults sets the defaults for a golang build and returns it. 37 func (*Builder) WithDefaults(build config.Build) (config.Build, error) { 38 if build.Dir == "" { 39 build.Dir = "." 40 } 41 if build.Main == "" { 42 build.Main = "." 43 } 44 if len(build.Ldflags) == 0 { 45 build.Ldflags = []string{"-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser"} 46 } 47 if len(build.Targets) == 0 { 48 if len(build.Goos) == 0 { 49 build.Goos = []string{"linux", "darwin"} 50 } 51 if len(build.Goarch) == 0 { 52 build.Goarch = []string{"amd64", "arm64", "386"} 53 } 54 if len(build.Goarm) == 0 { 55 build.Goarm = []string{"6"} 56 } 57 targets, err := matrix(build) 58 build.Targets = targets 59 if err != nil { 60 return build, err 61 } 62 } 63 if build.GoBinary == "" { 64 build.GoBinary = "go" 65 } 66 return build, nil 67 } 68 69 // Build builds a golang build. 70 func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error { 71 if err := checkMain(build); err != nil { 72 return err 73 } 74 target, err := newBuildTarget(options.Target) 75 if err != nil { 76 return err 77 } 78 79 var cmd = []string{build.GoBinary, "build"} 80 81 var env = append(ctx.Env.Strings(), build.Env...) 82 env = append(env, target.Env()...) 83 84 artifact := &artifact.Artifact{ 85 Type: artifact.Binary, 86 Path: options.Path, 87 Name: options.Name, 88 Goos: target.os, 89 Goarch: target.arch, 90 Goarm: target.arm, 91 Gomips: target.mips, 92 Extra: map[string]interface{}{ 93 "Binary": strings.TrimSuffix(filepath.Base(options.Path), options.Ext), 94 "Ext": options.Ext, 95 "ID": build.ID, 96 }, 97 } 98 99 flags, err := processFlags(ctx, artifact, env, build.Flags, "") 100 if err != nil { 101 return err 102 } 103 cmd = append(cmd, flags...) 104 105 asmflags, err := processFlags(ctx, artifact, env, build.Asmflags, "-asmflags=") 106 if err != nil { 107 return err 108 } 109 cmd = append(cmd, asmflags...) 110 111 gcflags, err := processFlags(ctx, artifact, env, build.Gcflags, "-gcflags=") 112 if err != nil { 113 return err 114 } 115 cmd = append(cmd, gcflags...) 116 117 // flag prefix is skipped because ldflags need to output a single string 118 ldflags, err := processFlags(ctx, artifact, env, build.Ldflags, "") 119 if err != nil { 120 return err 121 } 122 // ldflags need to be single string in order to apply correctly 123 processedLdFlags := joinLdFlags(ldflags) 124 125 cmd = append(cmd, processedLdFlags) 126 127 cmd = append(cmd, "-o", options.Path, build.Main) 128 if err := run(ctx, cmd, env, build.Dir); err != nil { 129 return fmt.Errorf("failed to build for %s: %w", options.Target, err) 130 } 131 132 if build.ModTimestamp != "" { 133 modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(artifact, map[string]string{}).Apply(build.ModTimestamp) 134 if err != nil { 135 return err 136 } 137 modUnix, err := strconv.ParseInt(modTimestamp, 10, 64) 138 if err != nil { 139 return err 140 } 141 modTime := time.Unix(modUnix, 0) 142 err = os.Chtimes(options.Path, modTime, modTime) 143 if err != nil { 144 return fmt.Errorf("failed to change times for %s: %w", options.Target, err) 145 } 146 } 147 148 ctx.Artifacts.Add(artifact) 149 return nil 150 } 151 152 func processFlags(ctx *context.Context, a *artifact.Artifact, env, flags []string, flagPrefix string) ([]string, error) { 153 processed := make([]string, 0, len(flags)) 154 for _, rawFlag := range flags { 155 flag, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(a, map[string]string{}).Apply(rawFlag) 156 if err != nil { 157 return nil, err 158 } 159 processed = append(processed, flagPrefix+flag) 160 } 161 return processed, nil 162 } 163 164 func joinLdFlags(flags []string) string { 165 ldflagString := strings.Builder{} 166 ldflagString.WriteString("-ldflags=") 167 ldflagString.WriteString(strings.Join(flags, " ")) 168 169 return ldflagString.String() 170 } 171 172 func run(ctx *context.Context, command, env []string, dir string) error { 173 /* #nosec */ 174 var cmd = exec.CommandContext(ctx, command[0], command[1:]...) 175 var log = log.WithField("env", env).WithField("cmd", command) 176 cmd.Env = env 177 cmd.Dir = dir 178 log.Debug("running") 179 if out, err := cmd.CombinedOutput(); err != nil { 180 log.WithError(err).Debug("failed") 181 return errors.New(string(out)) 182 } 183 return nil 184 } 185 186 type buildTarget struct { 187 os, arch, arm, mips string 188 } 189 190 func newBuildTarget(s string) (buildTarget, error) { 191 var t = buildTarget{} 192 parts := strings.Split(s, "_") 193 if len(parts) < 2 { 194 return t, fmt.Errorf("%s is not a valid build target", s) 195 } 196 t.os = parts[0] 197 t.arch = parts[1] 198 if strings.HasPrefix(t.arch, "arm") && len(parts) == 3 { 199 t.arm = parts[2] 200 } 201 if strings.HasPrefix(t.arch, "mips") && len(parts) == 3 { 202 t.mips = parts[2] 203 } 204 return t, nil 205 } 206 207 func (b buildTarget) Env() []string { 208 return []string{ 209 "GOOS=" + b.os, 210 "GOARCH=" + b.arch, 211 "GOARM=" + b.arm, 212 "GOMIPS=" + b.mips, 213 "GOMIPS64=" + b.mips, 214 } 215 } 216 217 func checkMain(build config.Build) error { 218 var main = build.Main 219 if main == "" { 220 main = "." 221 } 222 if build.Dir != "" { 223 main = filepath.Join(build.Dir, main) 224 } 225 stat, ferr := os.Stat(main) 226 if ferr != nil { 227 return ferr 228 } 229 if stat.IsDir() { 230 packs, err := parser.ParseDir(token.NewFileSet(), main, fileFilter, 0) 231 if err != nil { 232 return fmt.Errorf("failed to parse dir: %s: %w", main, err) 233 } 234 for _, pack := range packs { 235 for _, file := range pack.Files { 236 if hasMain(file) { 237 return nil 238 } 239 } 240 } 241 return fmt.Errorf("build for %s does not contain a main function", build.Binary) 242 } 243 file, err := parser.ParseFile(token.NewFileSet(), main, nil, 0) 244 if err != nil { 245 return fmt.Errorf("failed to parse file: %s: %w", main, err) 246 } 247 if hasMain(file) { 248 return nil 249 } 250 return fmt.Errorf("build for %s does not contain a main function", build.Binary) 251 } 252 253 // TODO: can be removed once we migrate from go 1.15 to 1.16. 254 func fileFilter(info os.FileInfo) bool { 255 return !info.IsDir() 256 } 257 258 func hasMain(file *ast.File) bool { 259 for _, decl := range file.Decls { 260 fn, isFn := decl.(*ast.FuncDecl) 261 if !isFn { 262 continue 263 } 264 if fn.Name.Name == "main" && fn.Recv == nil { 265 return true 266 } 267 } 268 return false 269 }