diff --git a/build/build.go b/build/build.go new file mode 100644 index 000000000..d121e02c2 --- /dev/null +++ b/build/build.go @@ -0,0 +1,37 @@ +// Package build provides the API for external builders +package build + +import ( + "sync" + + "github.com/goreleaser/goreleaser/config" + "github.com/goreleaser/goreleaser/context" +) + +var ( + builders = map[string]Builder{} + lock sync.Mutex +) + +// Register registers a builder to a given lang +func Register(lang string, builder Builder) { + lock.Lock() + builders[lang] = builder + lock.Unlock() +} + +// For gets the previously registered builder for the given lang +func For(lang string) Builder { + return builders[lang] +} + +// Options to be passed down to a builder +type Options struct { + Name, Path, Ext, Target string +} + +// Builder defines a builder +type Builder interface { + WithDefaults(build config.Build) config.Build + Build(ctx *context.Context, build config.Build, options Options) error +} diff --git a/build/build_test.go b/build/build_test.go new file mode 100644 index 000000000..382250b2d --- /dev/null +++ b/build/build_test.go @@ -0,0 +1,24 @@ +package build + +import ( + "testing" + + "github.com/goreleaser/goreleaser/config" + "github.com/goreleaser/goreleaser/context" + "github.com/stretchr/testify/assert" +) + +type dummy struct{} + +func (*dummy) WithDefaults(build config.Build) config.Build { + return build +} +func (*dummy) Build(ctx *context.Context, build config.Build, options Options) error { + return nil +} + +func TestRegisterAndGet(t *testing.T) { + var builder = &dummy{} + Register("dummy", builder) + assert.Equal(t, builder, For("dummy")) +} diff --git a/config/config.go b/config/config.go index 73571fc6c..697d96a27 100644 --- a/config/config.go +++ b/config/config.go @@ -71,6 +71,7 @@ type Build struct { Goos []string `yaml:",omitempty"` Goarch []string `yaml:",omitempty"` Goarm []string `yaml:",omitempty"` + Targets []string `yaml:",omitempty"` Ignore []IgnoredBuild `yaml:",omitempty"` Main string `yaml:",omitempty"` Ldflags string `yaml:",omitempty"` @@ -78,6 +79,7 @@ type Build struct { Binary string `yaml:",omitempty"` Hooks Hooks `yaml:",omitempty"` Env []string `yaml:",omitempty"` + Lang string `yaml:",omitempty"` } // FormatOverride is used to specify a custom format for a specific GOOS. diff --git a/internal/builders/golang/build.go b/internal/builders/golang/build.go new file mode 100644 index 000000000..626522310 --- /dev/null +++ b/internal/builders/golang/build.go @@ -0,0 +1,202 @@ +package golang + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "os/exec" + "strings" + "text/template" + "time" + + "github.com/apex/log" + api "github.com/goreleaser/goreleaser/build" + "github.com/goreleaser/goreleaser/config" + "github.com/goreleaser/goreleaser/context" + "github.com/goreleaser/goreleaser/internal/artifact" + "github.com/pkg/errors" +) + +// Default builder instance +var Default = &Builder{} + +func init() { + api.Register("go", Default) +} + +// Builder is golang builder +type Builder struct{} + +// WithDefaults sets the defaults for a golang build and returns it +func (*Builder) WithDefaults(build config.Build) config.Build { + if build.Main == "" { + build.Main = "." + } + if len(build.Goos) == 0 { + build.Goos = []string{"linux", "darwin"} + } + if len(build.Goarch) == 0 { + build.Goarch = []string{"amd64", "386"} + } + if len(build.Goarm) == 0 { + build.Goarm = []string{"6"} + } + if build.Ldflags == "" { + build.Ldflags = "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" + } + if len(build.Targets) == 0 { + build.Targets = matrix(build) + } + return build +} + +// Build builds a golang build +func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error { + if err := checkMain(ctx, build); err != nil { + return err + } + cmd := []string{"go", "build"} + if build.Flags != "" { + cmd = append(cmd, strings.Fields(build.Flags)...) + } + flags, err := ldflags(ctx, build) + if err != nil { + return err + } + cmd = append(cmd, "-ldflags="+flags, "-o", options.Path, build.Main) + target, err := newBuildTarget(options.Target) + if err != nil { + return err + } + var env = append(build.Env, target.Env()...) + if err := run(ctx, cmd, env); err != nil { + return errors.Wrapf(err, "failed to build for %s", options.Target) + } + ctx.Artifacts.Add(artifact.Artifact{ + Type: artifact.Binary, + Path: options.Path, + Name: options.Name, + Goos: target.os, + Goarch: target.arch, + Goarm: target.arm, + Extra: map[string]string{ + "Binary": build.Binary, + "Ext": options.Ext, + }, + }) + return nil +} + +func ldflags(ctx *context.Context, build config.Build) (string, error) { + var data = struct { + Commit string + Tag string + Version string + Date string + Env map[string]string + }{ + Commit: ctx.Git.Commit, + Tag: ctx.Git.CurrentTag, + Version: ctx.Version, + Date: time.Now().UTC().Format(time.RFC3339), + Env: ctx.Env, + } + var out bytes.Buffer + t, err := template.New("ldflags"). + Option("missingkey=error"). + Parse(build.Ldflags) + if err != nil { + return "", err + } + err = t.Execute(&out, data) + return out.String(), err +} + +func run(ctx *context.Context, command, env []string) error { + /* #nosec */ + var cmd = exec.CommandContext(ctx, command[0], command[1:]...) + var log = log.WithField("env", env).WithField("cmd", command) + cmd.Env = append(cmd.Env, os.Environ()...) + cmd.Env = append(cmd.Env, env...) + log.WithField("cmd", command).WithField("env", env).Debug("running") + if out, err := cmd.CombinedOutput(); err != nil { + log.WithError(err).Debug("failed") + return errors.New(string(out)) + } + return nil +} + +type buildTarget struct { + os, arch, arm string +} + +func newBuildTarget(s string) (buildTarget, error) { + var t = buildTarget{} + parts := strings.Split(s, "_") + if len(parts) < 2 { + return t, fmt.Errorf("%s is not a valid build target", s) + } + t.os = parts[0] + t.arch = parts[1] + if len(parts) == 3 { + t.arm = parts[2] + } + return t, nil +} + +func (b buildTarget) Env() []string { + return []string{ + "GOOS=" + b.os, + "GOARCH=" + b.arch, + "GOARM=" + b.arm, + } +} + +func checkMain(ctx *context.Context, build config.Build) error { + var main = build.Main + if main == "" { + main = "." + } + stat, ferr := os.Stat(main) + if os.IsNotExist(ferr) { + return errors.Wrapf(ferr, "could not open %s", main) + } + if stat.IsDir() { + packs, err := parser.ParseDir(token.NewFileSet(), main, nil, 0) + if err != nil { + return errors.Wrapf(err, "failed to parse dir: %s", main) + } + for _, pack := range packs { + for _, file := range pack.Files { + if hasMain(file) { + return nil + } + } + } + return fmt.Errorf("build for %s does not contain a main function", build.Binary) + } + file, err := parser.ParseFile(token.NewFileSet(), main, nil, 0) + if err != nil { + return errors.Wrapf(err, "failed to parse file: %s", main) + } + if hasMain(file) { + return nil + } + return fmt.Errorf("build for %s does not contain a main function", build.Binary) +} + +func hasMain(file *ast.File) bool { + for _, decl := range file.Decls { + fn, isFn := decl.(*ast.FuncDecl) + if !isFn { + continue + } + if fn.Name.Name == "main" && fn.Recv == nil { + return true + } + } + return false +} diff --git a/internal/builders/golang/build_test.go b/internal/builders/golang/build_test.go new file mode 100644 index 000000000..e4d451c4f --- /dev/null +++ b/internal/builders/golang/build_test.go @@ -0,0 +1,376 @@ +package golang + +import ( + "io/ioutil" + "path/filepath" + "runtime" + "strings" + "testing" + + api "github.com/goreleaser/goreleaser/build" + "github.com/goreleaser/goreleaser/config" + "github.com/goreleaser/goreleaser/context" + "github.com/goreleaser/goreleaser/internal/artifact" + "github.com/goreleaser/goreleaser/internal/testlib" + "github.com/stretchr/testify/assert" +) + +var runtimeTarget = runtime.GOOS + "_" + runtime.GOARCH + +func TestWithDefaults(t *testing.T) { + for name, testcase := range map[string]struct { + build config.Build + targets []string + }{ + "full": { + build: config.Build{ + Binary: "foo", + Goos: []string{ + "linux", + "windows", + "darwin", + }, + Goarch: []string{ + "amd64", + "arm", + }, + Goarm: []string{ + "6", + }, + }, + targets: []string{ + "linux_amd64", + "darwin_amd64", + "windows_amd64", + "linux_arm_6", + }, + }, + "empty": { + build: config.Build{ + Binary: "foo", + }, + targets: []string{ + "linux_amd64", + "linux_386", + "darwin_amd64", + "darwin_386", + }, + }, + } { + t.Run(name, func(tt *testing.T) { + var config = config.Project{ + Builds: []config.Build{ + testcase.build, + }, + } + var ctx = context.New(config) + var build = Default.WithDefaults(ctx.Config.Builds[0]) + assert.ElementsMatch(t, build.Targets, testcase.targets) + }) + } +} + +func TestBuild(t *testing.T) { + folder, back := testlib.Mktmp(t) + defer back() + writeGoodMain(t, folder) + var config = config.Project{ + Builds: []config.Build{ + { + Binary: "foo", + Targets: []string{ + "linux_amd64", + "darwin_amd64", + "windows_amd64", + "linux_arm_6", + }, + }, + }, + } + var ctx = context.New(config) + var build = ctx.Config.Builds[0] + for _, target := range build.Targets { + var ext string + if strings.HasPrefix(target, "windows") { + ext = ".exe" + } + var err = Default.Build(ctx, build, api.Options{ + Target: target, + Name: build.Binary, + Path: filepath.Join(folder, "dist", target, build.Binary), + Ext: ext, + }) + assert.NoError(t, err) + } + assert.ElementsMatch(t, ctx.Artifacts.List(), []artifact.Artifact{ + { + Name: "foo", + Path: filepath.Join(folder, "dist", "linux_amd64", "foo"), + Goos: "linux", + Goarch: "amd64", + Type: artifact.Binary, + Extra: map[string]string{ + "Ext": "", + "Binary": "foo", + }, + }, + { + Name: "foo", + Path: filepath.Join(folder, "dist", "darwin_amd64", "foo"), + Goos: "darwin", + Goarch: "amd64", + Type: artifact.Binary, + Extra: map[string]string{ + "Ext": "", + "Binary": "foo", + }, + }, + { + Name: "foo", + Path: filepath.Join(folder, "dist", "linux_arm_6", "foo"), + Goos: "linux", + Goarch: "arm", + Goarm: "6", + Type: artifact.Binary, + Extra: map[string]string{ + "Ext": "", + "Binary": "foo", + }, + }, + { + Name: "foo", + Path: filepath.Join(folder, "dist", "windows_amd64", "foo"), + Goos: "windows", + Goarch: "amd64", + Type: artifact.Binary, + Extra: map[string]string{ + "Ext": ".exe", + "Binary": "foo", + }, + }, + }) +} + +func TestBuildFailed(t *testing.T) { + folder, back := testlib.Mktmp(t) + defer back() + writeGoodMain(t, folder) + var config = config.Project{ + Builds: []config.Build{ + { + Flags: "-flag-that-dont-exists-to-force-failure", + Targets: []string{ + runtimeTarget, + }, + }, + }, + } + var ctx = context.New(config) + var err = Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: "darwin_amd64", + }) + assertContainsError(t, err, `flag provided but not defined: -flag-that-dont-exists-to-force-failure`) + assert.Empty(t, ctx.Artifacts.List()) +} + +func TestBuildInvalidTarget(t *testing.T) { + folder, back := testlib.Mktmp(t) + defer back() + writeGoodMain(t, folder) + var target = "linux" + var config = config.Project{ + Builds: []config.Build{ + { + Binary: "foo", + Targets: []string{target}, + }, + }, + } + var ctx = context.New(config) + var build = ctx.Config.Builds[0] + var err = Default.Build(ctx, build, api.Options{ + Target: target, + Name: build.Binary, + Path: filepath.Join(folder, "dist", target, build.Binary), + }) + assert.EqualError(t, err, "linux is not a valid build target") + assert.Len(t, ctx.Artifacts.List(), 0) +} + +func TestRunInvalidLdflags(t *testing.T) { + folder, back := testlib.Mktmp(t) + defer back() + writeGoodMain(t, folder) + var config = config.Project{ + Builds: []config.Build{ + { + Binary: "nametest", + Flags: "-v", + Ldflags: "-s -w -X main.version={{.Version}", + Targets: []string{ + runtimeTarget, + }, + }, + }, + } + var ctx = context.New(config) + var err = Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: runtimeTarget, + }) + assert.EqualError(t, err, `template: ldflags:1: unexpected "}" in operand`) +} + +func TestRunPipeWithoutMainFunc(t *testing.T) { + folder, back := testlib.Mktmp(t) + defer back() + writeMainWithoutMainFunc(t, folder) + var config = config.Project{ + Builds: []config.Build{ + { + Binary: "no-main", + Hooks: config.Hooks{}, + Targets: []string{ + runtimeTarget, + }, + }, + }, + } + var ctx = context.New(config) + t.Run("empty", func(t *testing.T) { + ctx.Config.Builds[0].Main = "" + assert.EqualError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: runtimeTarget, + }), `build for no-main does not contain a main function`) + }) + t.Run("not main.go", func(t *testing.T) { + ctx.Config.Builds[0].Main = "foo.go" + assert.EqualError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: runtimeTarget, + }), `could not open foo.go: stat foo.go: no such file or directory`) + }) + t.Run("glob", func(t *testing.T) { + ctx.Config.Builds[0].Main = "." + assert.EqualError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: runtimeTarget, + }), `build for no-main does not contain a main function`) + }) + t.Run("fixed main.go", func(t *testing.T) { + ctx.Config.Builds[0].Main = "main.go" + assert.EqualError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: runtimeTarget, + }), `build for no-main does not contain a main function`) + }) +} + +func TestRunPipeWithMainFuncNotInMainGoFile(t *testing.T) { + folder, back := testlib.Mktmp(t) + defer back() + assert.NoError(t, ioutil.WriteFile( + filepath.Join(folder, "foo.go"), + []byte("package main\nfunc main() {println(0)}"), + 0644, + )) + var config = config.Project{ + Builds: []config.Build{ + { + Binary: "foo", + Hooks: config.Hooks{}, + Targets: []string{ + runtimeTarget, + }, + }, + }, + } + var ctx = context.New(config) + t.Run("empty", func(t *testing.T) { + ctx.Config.Builds[0].Main = "" + assert.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: runtimeTarget, + })) + }) + t.Run("foo.go", func(t *testing.T) { + ctx.Config.Builds[0].Main = "foo.go" + assert.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: runtimeTarget, + })) + }) + t.Run("glob", func(t *testing.T) { + ctx.Config.Builds[0].Main = "." + assert.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ + Target: runtimeTarget, + })) + }) +} + +func TestLdFlagsFullTemplate(t *testing.T) { + var config = config.Project{ + Builds: []config.Build{ + { + Ldflags: `-s -w -X main.version={{.Version}} -X main.tag={{.Tag}} -X main.date={{.Date}} -X main.commit={{.Commit}} -X "main.foo={{.Env.FOO}}"`, + }, + }, + } + var ctx = &context.Context{ + Git: context.GitInfo{ + CurrentTag: "v1.2.3", + Commit: "123", + }, + Version: "1.2.3", + Config: config, + Env: map[string]string{"FOO": "123"}, + } + flags, err := ldflags(ctx, ctx.Config.Builds[0]) + assert.NoError(t, err) + assert.Contains(t, flags, "-s -w") + assert.Contains(t, flags, "-X main.version=1.2.3") + assert.Contains(t, flags, "-X main.tag=v1.2.3") + assert.Contains(t, flags, "-X main.commit=123") + assert.Contains(t, flags, "-X main.date=") + assert.Contains(t, flags, `-X "main.foo=123"`) +} + +func TestInvalidTemplate(t *testing.T) { + for template, eerr := range map[string]string{ + "{{ .Nope }": `template: ldflags:1: unexpected "}" in operand`, + "{{.Env.NOPE}}": `template: ldflags:1:6: executing "ldflags" at <.Env.NOPE>: map has no entry for key "NOPE"`, + } { + t.Run(template, func(tt *testing.T) { + var config = config.Project{ + Builds: []config.Build{ + {Ldflags: template}, + }, + } + var ctx = &context.Context{ + Config: config, + } + flags, err := ldflags(ctx, ctx.Config.Builds[0]) + assert.EqualError(tt, err, eerr) + assert.Empty(tt, flags) + }) + } +} + +// +// Helpers +// + +func writeMainWithoutMainFunc(t *testing.T, folder string) { + assert.NoError(t, ioutil.WriteFile( + filepath.Join(folder, "main.go"), + []byte("package main\nconst a = 2\nfunc notMain() {println(0)}"), + 0644, + )) +} + +func writeGoodMain(t *testing.T, folder string) { + assert.NoError(t, ioutil.WriteFile( + filepath.Join(folder, "main.go"), + []byte("package main\nvar a = 1\nfunc main() {println(0)}"), + 0644, + )) +} + +func assertContainsError(t *testing.T, err error, s string) { + assert.Error(t, err) + assert.Contains(t, err.Error(), s) +} diff --git a/internal/builders/golang/doc.go b/internal/builders/golang/doc.go new file mode 100644 index 000000000..574b52a90 --- /dev/null +++ b/internal/builders/golang/doc.go @@ -0,0 +1,2 @@ +// Package golang provides a Builder implementation for golang. +package golang diff --git a/internal/buildtarget/targets.go b/internal/builders/golang/targets.go similarity index 57% rename from internal/buildtarget/targets.go rename to internal/builders/golang/targets.go index 2e5af3960..e30a017c0 100644 --- a/internal/buildtarget/targets.go +++ b/internal/builders/golang/targets.go @@ -1,52 +1,70 @@ -package buildtarget +package golang import ( + "fmt" + "github.com/apex/log" "github.com/goreleaser/goreleaser/config" ) -// All returns all valid build targets for a given build -func All(build config.Build) (targets []Target) { +type target struct { + os, arch, arm string +} + +func (t target) String() string { + if t.arm != "" { + return fmt.Sprintf("%s_%s_%s", t.os, t.arch, t.arm) + } + return fmt.Sprintf("%s_%s", t.os, t.arch) +} + +func matrix(build config.Build) (result []string) { + var targets []target for _, target := range allBuildTargets(build) { if !valid(target) { - log.WithField("target", target.PrettyString()). + log.WithField("target", target). Debug("skipped invalid build") continue } if ignored(build, target) { - log.WithField("target", target.PrettyString()). + log.WithField("target", target). Debug("skipped ignored build") continue } targets = append(targets, target) } + for _, target := range targets { + result = append(result, target.String()) + } return } -func allBuildTargets(build config.Build) (targets []Target) { +func allBuildTargets(build config.Build) (targets []target) { for _, goos := range build.Goos { for _, goarch := range build.Goarch { if goarch == "arm" { for _, goarm := range build.Goarm { - targets = append(targets, New(goos, goarch, goarm)) + targets = append(targets, target{goos, goarch, goarm}) } continue } - targets = append(targets, New(goos, goarch, "")) + targets = append(targets, target{goos, goarch, ""}) } } return } -func ignored(build config.Build, target Target) bool { +// TODO: this could be improved by using a map +// https://github.com/goreleaser/goreleaser/pull/522#discussion_r164245014 +func ignored(build config.Build, target target) bool { for _, ig := range build.Ignore { - if ig.Goos != "" && ig.Goos != target.OS { + if ig.Goos != "" && ig.Goos != target.os { continue } - if ig.Goarch != "" && ig.Goarch != target.Arch { + if ig.Goarch != "" && ig.Goarch != target.arch { continue } - if ig.Goarm != "" && ig.Goarm != target.Arm { + if ig.Goarm != "" && ig.Goarm != target.arm { continue } return true @@ -54,8 +72,8 @@ func ignored(build config.Build, target Target) bool { return false } -func valid(target Target) bool { - var s = target.OS + target.Arch +func valid(target target) bool { + var s = target.os + target.arch for _, a := range validTargets { if a == s { return true diff --git a/internal/buildtarget/targets_test.go b/internal/builders/golang/targets_test.go similarity index 80% rename from internal/buildtarget/targets_test.go rename to internal/builders/golang/targets_test.go index 5758b2eed..42251bc57 100644 --- a/internal/buildtarget/targets_test.go +++ b/internal/builders/golang/targets_test.go @@ -1,4 +1,4 @@ -package buildtarget +package golang import ( "fmt" @@ -40,19 +40,19 @@ func TestAllBuildTargets(t *testing.T) { }, }, } - assert.Equal(t, []Target{ - New("linux", "386", ""), - New("linux", "amd64", ""), - New("linux", "arm", "6"), - New("linux", "arm64", ""), - New("darwin", "amd64", ""), - New("freebsd", "386", ""), - New("freebsd", "amd64", ""), - New("freebsd", "arm", "6"), - New("freebsd", "arm", "7"), - New("openbsd", "386", ""), - New("openbsd", "amd64", ""), - }, All(build)) + assert.Equal(t, []string{ + "linux_386", + "linux_amd64", + "linux_arm_6", + "linux_arm64", + "darwin_amd64", + "freebsd_386", + "freebsd_amd64", + "freebsd_arm_6", + "freebsd_arm_7", + "openbsd_386", + "openbsd_amd64", + }, matrix(build)) } func TestGoosGoarchCombos(t *testing.T) { @@ -99,7 +99,7 @@ func TestGoosGoarchCombos(t *testing.T) { } for _, p := range platforms { t.Run(fmt.Sprintf("%v %v valid=%v", p.os, p.arch, p.valid), func(t *testing.T) { - assert.Equal(t, p.valid, valid(New(p.os, p.arch, ""))) + assert.Equal(t, p.valid, valid(target{p.os, p.arch, ""})) }) } } diff --git a/internal/buildtarget/buildtarget.go b/internal/buildtarget/buildtarget.go deleted file mode 100644 index a2e4f6915..000000000 --- a/internal/buildtarget/buildtarget.go +++ /dev/null @@ -1,38 +0,0 @@ -package buildtarget - -import ( - "fmt" - "runtime" -) - -// Runtime is the current runtime build target -var Runtime = Target{runtime.GOOS, runtime.GOARCH, ""} - -// New build Target -func New(goos, goarch, goarm string) Target { - return Target{goos, goarch, goarm} -} - -// Target is a build target -type Target struct { - OS, Arch, Arm string -} - -// Env returns the current Target as environment variables -func (t Target) Env() []string { - return []string{ - "GOOS=" + t.OS, - "GOARCH=" + t.Arch, - "GOARM=" + t.Arm, - } -} - -func (t Target) String() string { - // TODO: maybe replace this as suggested to OS_ArchArm? - return fmt.Sprintf("%v%v%v", t.OS, t.Arch, t.Arm) -} - -// PrettyString is a prettier version of the String method. -func (t Target) PrettyString() string { - return fmt.Sprintf("%v/%v%v", t.OS, t.Arch, t.Arm) -} diff --git a/internal/buildtarget/buildtarget_test.go b/internal/buildtarget/buildtarget_test.go deleted file mode 100644 index 11c4f732d..000000000 --- a/internal/buildtarget/buildtarget_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package buildtarget - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEnv(t *testing.T) { - assert.Equal( - t, - []string{"GOOS=linux", "GOARCH=arm64", "GOARM=6"}, - New("linux", "arm64", "6").Env(), - ) -} - -func TestString(t *testing.T) { - assert.Equal( - t, - "linuxarm7", - New("linux", "arm", "7").String(), - ) -} - -func TestPrettyString(t *testing.T) { - assert.Equal( - t, - "linux/arm646", - New("linux", "arm64", "6").PrettyString(), - ) -} diff --git a/internal/buildtarget/doc.go b/internal/buildtarget/doc.go deleted file mode 100644 index 828f8ecf5..000000000 --- a/internal/buildtarget/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package buildtarget provides the utilities targeting build matrixes. -// TODO: probably this package should be removed and used only inside the build package -package buildtarget diff --git a/internal/ext/ext.go b/internal/ext/ext.go deleted file mode 100644 index 583d26743..000000000 --- a/internal/ext/ext.go +++ /dev/null @@ -1,11 +0,0 @@ -package ext - -import "github.com/goreleaser/goreleaser/internal/buildtarget" - -// For returns the binary extension for the given platform -func For(target buildtarget.Target) string { - if target.OS == "windows" { - return ".exe" - } - return "" -} diff --git a/internal/ext/ext_test.go b/internal/ext/ext_test.go deleted file mode 100644 index 41e6c120e..000000000 --- a/internal/ext/ext_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package ext - -import ( - "testing" - - "github.com/goreleaser/goreleaser/internal/buildtarget" - "github.com/stretchr/testify/assert" -) - -func TestExtWindows(t *testing.T) { - assert.Equal(t, ".exe", For(buildtarget.New("windows", "", ""))) - assert.Equal(t, ".exe", For(buildtarget.New("windows", "adm64", ""))) -} - -func TestExtOthers(t *testing.T) { - assert.Empty(t, "", For(buildtarget.New("linux", "", ""))) - assert.Empty(t, "", For(buildtarget.New("linuxwin", "", ""))) - assert.Empty(t, "", For(buildtarget.New("winasdasd", "sad", "6"))) -} diff --git a/pipeline/build/build.go b/pipeline/build/build.go index c32772860..f04477c1c 100644 --- a/pipeline/build/build.go +++ b/pipeline/build/build.go @@ -1,3 +1,5 @@ +// Package build provides a pipe that can build binaries for several +// languages. package build import ( @@ -10,11 +12,12 @@ import ( "github.com/pkg/errors" "golang.org/x/sync/errgroup" + builders "github.com/goreleaser/goreleaser/build" "github.com/goreleaser/goreleaser/config" "github.com/goreleaser/goreleaser/context" - "github.com/goreleaser/goreleaser/internal/artifact" - "github.com/goreleaser/goreleaser/internal/buildtarget" - "github.com/goreleaser/goreleaser/internal/ext" + + // langs to init + _ "github.com/goreleaser/goreleaser/internal/builders/golang" ) // Pipe for build @@ -28,9 +31,6 @@ func (Pipe) String() string { func (Pipe) Run(ctx *context.Context) error { for _, build := range ctx.Config.Builds { log.WithField("build", build).Debug("building") - if err := checkMain(ctx, build); err != nil { - return err - } if err := runPipeOnBuild(ctx, build); err != nil { return err } @@ -52,25 +52,13 @@ func (Pipe) Default(ctx *context.Context) error { } func buildWithDefaults(ctx *context.Context, build config.Build) config.Build { + if build.Lang == "" { + build.Lang = "go" + } if build.Binary == "" { build.Binary = ctx.Config.Release.GitHub.Name } - if build.Main == "" { - build.Main = "." - } - if len(build.Goos) == 0 { - build.Goos = []string{"linux", "darwin"} - } - if len(build.Goarch) == 0 { - build.Goarch = []string{"amd64", "386"} - } - if len(build.Goarm) == 0 { - build.Goarm = []string{"6"} - } - if build.Ldflags == "" { - build.Ldflags = "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - } - return build + return builders.For(build.Lang).WithDefaults(build) } func runPipeOnBuild(ctx *context.Context, build config.Build) error { @@ -79,7 +67,7 @@ func runPipeOnBuild(ctx *context.Context, build config.Build) error { } sem := make(chan bool, ctx.Parallelism) var g errgroup.Group - for _, target := range buildtarget.All(build) { + for _, target := range build.Targets { sem <- true target := target build := build @@ -102,51 +90,36 @@ func runHook(ctx *context.Context, env []string, hook string) error { } log.WithField("hook", hook).Info("running hook") cmd := strings.Fields(hook) - return run(ctx, buildtarget.Runtime, cmd, env) + return run(ctx, cmd, env) } -func doBuild(ctx *context.Context, build config.Build, target buildtarget.Target) error { - var ext = ext.For(target) - var binaryName = build.Binary + ext - var binary = filepath.Join(ctx.Config.Dist, target.String(), binaryName) - log.WithField("binary", binary).Info("building") - cmd := []string{"go", "build"} - if build.Flags != "" { - cmd = append(cmd, strings.Fields(build.Flags)...) - } - flags, err := ldflags(ctx, build) - if err != nil { - return err - } - cmd = append(cmd, "-ldflags="+flags, "-o", binary, build.Main) - if err := run(ctx, target, cmd, build.Env); err != nil { - return errors.Wrapf(err, "failed to build for %s", target) - } - ctx.Artifacts.Add(artifact.Artifact{ - Type: artifact.Binary, - Path: binary, - Name: binaryName, - Goos: target.OS, - Goarch: target.Arch, - Goarm: target.Arm, - Extra: map[string]string{ - "Binary": build.Binary, - "Ext": ext, - }, +func doBuild(ctx *context.Context, build config.Build, target string) error { + var ext = extFor(target) + var name = build.Binary + ext + var path = filepath.Join(ctx.Config.Dist, target, name) + log.WithField("binary", path).Info("building") + return builders.For(build.Lang).Build(ctx, build, builders.Options{ + Target: target, + Name: name, + Path: path, + Ext: ext, }) - return nil } -func run(ctx *context.Context, target buildtarget.Target, command, env []string) error { +func extFor(target string) string { + if strings.Contains(target, "windows") { + return ".exe" + } + return "" +} + +func run(ctx *context.Context, command, env []string) error { /* #nosec */ var cmd = exec.CommandContext(ctx, command[0], command[1:]...) - env = append(env, target.Env()...) - var log = log.WithField("target", target.PrettyString()). - WithField("env", env). - WithField("cmd", command) + var log = log.WithField("env", env).WithField("cmd", command) cmd.Env = append(cmd.Env, os.Environ()...) cmd.Env = append(cmd.Env, env...) - log.Debug("running") + log.WithField("cmd", command).WithField("env", env).Debug("running") if out, err := cmd.CombinedOutput(); err != nil { log.WithError(err).Debug("failed") return errors.New(string(out)) diff --git a/pipeline/build/build_test.go b/pipeline/build/build_test.go index bc0e6e8ba..89a050dba 100644 --- a/pipeline/build/build_test.go +++ b/pipeline/build/build_test.go @@ -1,47 +1,57 @@ package build import ( - "io/ioutil" + "errors" "os" "path/filepath" - "runtime" "testing" + api "github.com/goreleaser/goreleaser/build" "github.com/goreleaser/goreleaser/config" "github.com/goreleaser/goreleaser/context" - "github.com/goreleaser/goreleaser/internal/buildtarget" + "github.com/goreleaser/goreleaser/internal/artifact" "github.com/goreleaser/goreleaser/internal/testlib" "github.com/stretchr/testify/assert" ) -var emptyEnv []string +var fakeArtifact = artifact.Artifact{ + Name: "fake", +} + +type fakeBuilder struct { + fail bool +} + +func (*fakeBuilder) WithDefaults(build config.Build) config.Build { + return build +} + +var errFailedBuild = errors.New("fake builder failed") + +func (f *fakeBuilder) Build(ctx *context.Context, build config.Build, options api.Options) error { + if f.fail { + return errFailedBuild + } + ctx.Artifacts.Add(fakeArtifact) + return nil +} + +func init() { + api.Register("fake", &fakeBuilder{}) + api.Register("fakeFail", &fakeBuilder{ + fail: true, + }) +} func TestPipeDescription(t *testing.T) { assert.NotEmpty(t, Pipe{}.String()) } -func TestRun(t *testing.T) { - assert.NoError(t, run( - context.New(config.Project{}), - buildtarget.Runtime, - []string{"go", "list", "./..."}, - emptyEnv, - )) -} - -func TestRunInvalidCommand(t *testing.T) { - assert.Error(t, run( - context.New(config.Project{}), - buildtarget.Runtime, - []string{"gggggo", "nope"}, - emptyEnv, - )) -} - func TestBuild(t *testing.T) { var config = config.Project{ Builds: []config.Build{ { + Lang: "fake", Binary: "testing", Flags: "-n", Env: []string{"BLAH=1"}, @@ -49,19 +59,35 @@ func TestBuild(t *testing.T) { }, } var ctx = context.New(config) - assert.NoError(t, doBuild(ctx, ctx.Config.Builds[0], buildtarget.Runtime)) + assert.NoError(t, doBuild(ctx, ctx.Config.Builds[0], "darwin_amd64")) +} + +func TestRunPipe(t *testing.T) { + var config = config.Project{ + Builds: []config.Build{ + { + Lang: "fake", + Binary: "testing", + Flags: "-v", + Ldflags: "-X main.test=testing", + Targets: []string{"whatever"}, + }, + }, + } + var ctx = context.New(config) + assert.NoError(t, Pipe{}.Run(ctx)) + assert.Equal(t, ctx.Artifacts.List(), []artifact.Artifact{fakeArtifact}) } func TestRunFullPipe(t *testing.T) { folder, back := testlib.Mktmp(t) defer back() - writeGoodMain(t, folder) - var binary = filepath.Join(folder, buildtarget.Runtime.String(), "testing") var pre = filepath.Join(folder, "pre") var post = filepath.Join(folder, "post") var config = config.Project{ Builds: []config.Build{ { + Lang: "fake", Binary: "testing", Flags: "-v", Ldflags: "-X main.test=testing", @@ -69,138 +95,52 @@ func TestRunFullPipe(t *testing.T) { Pre: "touch " + pre, Post: "touch " + post, }, - Goos: []string{ - runtime.GOOS, - }, - Goarch: []string{ - runtime.GOARCH, - }, - }, - }, - Archive: config.Archive{ - Replacements: map[string]string{ - "linux": "linuxx", - "darwin": "darwinn", + Targets: []string{"whatever"}, }, }, } var ctx = context.New(config) assert.NoError(t, Pipe{}.Run(ctx)) - assert.Len(t, ctx.Artifacts.List(), 1) - assert.True(t, exists(binary), binary) + assert.Equal(t, ctx.Artifacts.List(), []artifact.Artifact{fakeArtifact}) assert.True(t, exists(pre), pre) assert.True(t, exists(post), post) } -func TestRunPipeArmBuilds(t *testing.T) { +func TestRunFullPipeFail(t *testing.T) { folder, back := testlib.Mktmp(t) defer back() - writeGoodMain(t, folder) - var binary = filepath.Join(folder, "linuxarm6", "armtesting") + var pre = filepath.Join(folder, "pre") + var post = filepath.Join(folder, "post") var config = config.Project{ Builds: []config.Build{ { - Binary: "armtesting", + Lang: "fakeFail", + Binary: "testing", Flags: "-v", - Ldflags: "-X main.test=armtesting", - Goos: []string{ - "linux", - }, - Goarch: []string{ - "arm", - "arm64", - }, - Goarm: []string{ - "6", + Ldflags: "-X main.test=testing", + Hooks: config.Hooks{ + Pre: "touch " + pre, + Post: "touch " + post, }, + Targets: []string{"whatever"}, }, }, } var ctx = context.New(config) - assert.NoError(t, Pipe{}.Run(ctx)) - assert.Len(t, ctx.Artifacts.List(), 2) - assert.True(t, exists(binary), binary) -} - -func TestBuildFailed(t *testing.T) { - folder, back := testlib.Mktmp(t) - defer back() - writeGoodMain(t, folder) - var config = config.Project{ - Builds: []config.Build{ - { - Flags: "-flag-that-dont-exists-to-force-failure", - Goos: []string{ - runtime.GOOS, - }, - Goarch: []string{ - runtime.GOARCH, - }, - }, - }, - } - var ctx = context.New(config) - assertContainsError(t, Pipe{}.Run(ctx), `flag provided but not defined: -flag-that-dont-exists-to-force-failure`) + assert.EqualError(t, Pipe{}.Run(ctx), errFailedBuild.Error()) assert.Empty(t, ctx.Artifacts.List()) -} - -func TestRunPipeWithInvalidOS(t *testing.T) { - folder, back := testlib.Mktmp(t) - defer back() - writeGoodMain(t, folder) - var config = config.Project{ - Builds: []config.Build{ - { - Flags: "-v", - Goos: []string{ - "windows", - }, - Goarch: []string{ - "arm", - }, - }, - }, - } - assert.NoError(t, Pipe{}.Run(context.New(config))) -} - -func TestRunInvalidLdflags(t *testing.T) { - folder, back := testlib.Mktmp(t) - defer back() - writeGoodMain(t, folder) - var config = config.Project{ - Builds: []config.Build{ - { - Binary: "nametest", - Flags: "-v", - Ldflags: "-s -w -X main.version={{.Version}", - Goos: []string{ - runtime.GOOS, - }, - Goarch: []string{ - runtime.GOARCH, - }, - }, - }, - } - assert.EqualError(t, Pipe{}.Run(context.New(config)), `template: ldflags:1: unexpected "}" in operand`) + assert.True(t, exists(pre), pre) + assert.False(t, exists(post), post) } func TestRunPipeFailingHooks(t *testing.T) { - folder, back := testlib.Mktmp(t) - defer back() - writeGoodMain(t, folder) var config = config.Project{ Builds: []config.Build{ { - Binary: "hooks", - Hooks: config.Hooks{}, - Goos: []string{ - runtime.GOOS, - }, - Goarch: []string{ - runtime.GOARCH, - }, + Lang: "fake", + Binary: "hooks", + Hooks: config.Hooks{}, + Targets: []string{"whatever"}, }, }, } @@ -218,80 +158,6 @@ func TestRunPipeFailingHooks(t *testing.T) { }) } -func TestRunPipeWithouMainFunc(t *testing.T) { - folder, back := testlib.Mktmp(t) - defer back() - writeMainWithoutMainFunc(t, folder) - var config = config.Project{ - Builds: []config.Build{ - { - Binary: "no-main", - Hooks: config.Hooks{}, - Goos: []string{ - runtime.GOOS, - }, - Goarch: []string{ - runtime.GOARCH, - }, - }, - }, - } - var ctx = context.New(config) - t.Run("empty", func(t *testing.T) { - ctx.Config.Builds[0].Main = "" - assert.EqualError(t, Pipe{}.Run(ctx), `build for no-main does not contain a main function`) - }) - t.Run("not main.go", func(t *testing.T) { - ctx.Config.Builds[0].Main = "foo.go" - assert.EqualError(t, Pipe{}.Run(ctx), `could not open foo.go: stat foo.go: no such file or directory`) - }) - t.Run("glob", func(t *testing.T) { - ctx.Config.Builds[0].Main = "." - assert.EqualError(t, Pipe{}.Run(ctx), `build for no-main does not contain a main function`) - }) - t.Run("fixed main.go", func(t *testing.T) { - ctx.Config.Builds[0].Main = "main.go" - assert.EqualError(t, Pipe{}.Run(ctx), `build for no-main does not contain a main function`) - }) -} - -func TestRunPipeWithMainFuncNotInMainGoFile(t *testing.T) { - folder, back := testlib.Mktmp(t) - defer back() - assert.NoError(t, ioutil.WriteFile( - filepath.Join(folder, "foo.go"), - []byte("package main\nfunc main() {println(0)}"), - 0644, - )) - var config = config.Project{ - Builds: []config.Build{ - { - Binary: "foo", - Hooks: config.Hooks{}, - Goos: []string{ - runtime.GOOS, - }, - Goarch: []string{ - runtime.GOARCH, - }, - }, - }, - } - var ctx = context.New(config) - t.Run("empty", func(t *testing.T) { - ctx.Config.Builds[0].Main = "" - assert.NoError(t, Pipe{}.Run(ctx)) - }) - t.Run("foo.go", func(t *testing.T) { - ctx.Config.Builds[0].Main = "foo.go" - assert.NoError(t, Pipe{}.Run(ctx)) - }) - t.Run("glob", func(t *testing.T) { - ctx.Config.Builds[0].Main = "." - assert.NoError(t, Pipe{}.Run(ctx)) - }) -} - func TestDefaultNoBuilds(t *testing.T) { var ctx = &context.Context{ Config: config.Project{}, @@ -381,28 +247,22 @@ func TestDefaultFillSingleBuild(t *testing.T) { assert.Equal(t, ctx.Config.Builds[0].Binary, "foo") } +func TestExtWindows(t *testing.T) { + assert.Equal(t, ".exe", extFor("windows_amd64")) + assert.Equal(t, ".exe", extFor("windows_386")) +} + +func TestExtOthers(t *testing.T) { + assert.Empty(t, "", extFor("linux_amd64")) + assert.Empty(t, "", extFor("linuxwin_386")) + assert.Empty(t, "", extFor("winasdasd_sad")) +} + +// +// Helpers +// + func exists(file string) bool { _, err := os.Stat(file) return !os.IsNotExist(err) } - -func writeMainWithoutMainFunc(t *testing.T, folder string) { - assert.NoError(t, ioutil.WriteFile( - filepath.Join(folder, "main.go"), - []byte("package main\nconst a = 2\nfunc notMain() {println(0)}"), - 0644, - )) -} - -func writeGoodMain(t *testing.T, folder string) { - assert.NoError(t, ioutil.WriteFile( - filepath.Join(folder, "main.go"), - []byte("package main\nvar a = 1\nfunc main() {println(0)}"), - 0644, - )) -} - -func assertContainsError(t *testing.T, err error, s string) { - assert.Error(t, err) - assert.Contains(t, err.Error(), s) -} diff --git a/pipeline/build/checkmain.go b/pipeline/build/checkmain.go deleted file mode 100644 index f89fde77b..000000000 --- a/pipeline/build/checkmain.go +++ /dev/null @@ -1,59 +0,0 @@ -package build - -import ( - "fmt" - "go/ast" - "go/parser" - "go/token" - "os" - - "github.com/goreleaser/goreleaser/config" - "github.com/goreleaser/goreleaser/context" - "github.com/pkg/errors" -) - -func checkMain(ctx *context.Context, build config.Build) error { - var main = build.Main - if main == "" { - main = "." - } - stat, ferr := os.Stat(main) - if os.IsNotExist(ferr) { - return errors.Wrapf(ferr, "could not open %s", main) - } - if stat.IsDir() { - packs, err := parser.ParseDir(token.NewFileSet(), main, nil, 0) - if err != nil { - return errors.Wrapf(err, "failed to parse dir: %s", main) - } - for _, pack := range packs { - for _, file := range pack.Files { - if hasMain(file) { - return nil - } - } - } - return fmt.Errorf("build for %s does not contain a main function", build.Binary) - } - file, err := parser.ParseFile(token.NewFileSet(), build.Main, nil, 0) - if err != nil { - return errors.Wrapf(err, "failed to parse file: %s", build.Main) - } - if hasMain(file) { - return nil - } - return fmt.Errorf("build for %s does not contain a main function", build.Binary) -} - -func hasMain(file *ast.File) bool { - for _, decl := range file.Decls { - fn, isFn := decl.(*ast.FuncDecl) - if !isFn { - continue - } - if fn.Name.Name == "main" && fn.Recv == nil { - return true - } - } - return false -} diff --git a/pipeline/build/doc.go b/pipeline/build/doc.go deleted file mode 100644 index e4d81c9af..000000000 --- a/pipeline/build/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package build implements Piper and Defaulter and can build Go projects for -// several platforms, with pre and post hook support. -// Build also checks wether the current project has a main function, parses -// ldflags and other goodies. -package build diff --git a/pipeline/build/ldflags.go b/pipeline/build/ldflags.go deleted file mode 100644 index 2bf0fa3ee..000000000 --- a/pipeline/build/ldflags.go +++ /dev/null @@ -1,37 +0,0 @@ -package build - -import ( - "bytes" - "text/template" - "time" - - "github.com/goreleaser/goreleaser/config" - "github.com/goreleaser/goreleaser/context" -) - -type ldflagsData struct { - Date string - Tag string - Commit string - Version string - Env map[string]string -} - -func ldflags(ctx *context.Context, build config.Build) (string, error) { - var data = ldflagsData{ - Commit: ctx.Git.Commit, - Tag: ctx.Git.CurrentTag, - Version: ctx.Version, - Date: time.Now().UTC().Format(time.RFC3339), - Env: ctx.Env, - } - var out bytes.Buffer - t, err := template.New("ldflags"). - Option("missingkey=error"). - Parse(build.Ldflags) - if err != nil { - return "", err - } - err = t.Execute(&out, data) - return out.String(), err -} diff --git a/pipeline/build/ldflags_test.go b/pipeline/build/ldflags_test.go deleted file mode 100644 index a4bf22fa2..000000000 --- a/pipeline/build/ldflags_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package build - -import ( - "testing" - - "github.com/goreleaser/goreleaser/config" - "github.com/goreleaser/goreleaser/context" - "github.com/stretchr/testify/assert" -) - -func TestLdFlagsFullTemplate(t *testing.T) { - var config = config.Project{ - Builds: []config.Build{ - { - Ldflags: `-s -w -X main.version={{.Version}} -X main.tag={{.Tag}} -X main.date={{.Date}} -X main.commit={{.Commit}} -X "main.foo={{.Env.FOO}}"`, - }, - }, - } - var ctx = &context.Context{ - Git: context.GitInfo{ - CurrentTag: "v1.2.3", - Commit: "123", - }, - Version: "1.2.3", - Config: config, - Env: map[string]string{"FOO": "123"}, - } - flags, err := ldflags(ctx, ctx.Config.Builds[0]) - assert.NoError(t, err) - assert.Contains(t, flags, "-s -w") - assert.Contains(t, flags, "-X main.version=1.2.3") - assert.Contains(t, flags, "-X main.tag=v1.2.3") - assert.Contains(t, flags, "-X main.commit=123") - assert.Contains(t, flags, "-X main.date=") - assert.Contains(t, flags, `-X "main.foo=123"`) -} - -func TestInvalidTemplate(t *testing.T) { - for template, eerr := range map[string]string{ - "{{ .Nope }": `template: ldflags:1: unexpected "}" in operand`, - "{{.Env.NOPE}}": `template: ldflags:1:6: executing "ldflags" at <.Env.NOPE>: map has no entry for key "NOPE"`, - } { - t.Run(template, func(tt *testing.T) { - var config = config.Project{ - Builds: []config.Build{ - {Ldflags: template}, - }, - } - var ctx = &context.Context{ - Config: config, - } - flags, err := ldflags(ctx, ctx.Config.Builds[0]) - assert.EqualError(tt, err, eerr) - assert.Empty(tt, flags) - }) - } -}