1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-24 04:16:27 +02:00

Merge pull request #522 from goreleaser/build

feat: support multiple build systems
This commit is contained in:
Carlos Alexandro Becker 2018-01-28 18:06:23 -02:00 committed by GitHub
commit 81ab872b3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 807 additions and 573 deletions

37
build/build.go Normal file
View File

@ -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
}

24
build/build_test.go Normal file
View File

@ -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"))
}

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}

View File

@ -0,0 +1,2 @@
// Package golang provides a Builder implementation for golang.
package golang

View File

@ -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

View File

@ -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, ""}))
})
}
}

View File

@ -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)
}

View File

@ -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(),
)
}

View File

@ -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

View File

@ -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 ""
}

View File

@ -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")))
}

View File

@ -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))

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
})
}
}