1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-02-07 13:31:37 +02:00

feat: support multiple build systems

This commit is contained in:
Carlos Alexandro Becker 2018-01-21 14:31:08 -02:00
parent 395dd1811c
commit a457ae6e17
No known key found for this signature in database
GPG Key ID: E61E2F7DC14AB940
17 changed files with 237 additions and 172 deletions

55
build/build.go Normal file
View File

@ -0,0 +1,55 @@
package build
import (
"errors"
"os"
"os/exec"
"sync"
"github.com/apex/log"
"github.com/goreleaser/goreleaser/build/buildtarget"
"github.com/goreleaser/goreleaser/config"
"github.com/goreleaser/goreleaser/context"
)
var (
builders = map[string]Builder{}
lock sync.Mutex
)
func Register(lang string, builder Builder) {
lock.Lock()
builders[lang] = builder
lock.Unlock()
}
func For(lang string) Builder {
return builders[lang]
}
type Options struct {
Target buildtarget.Target
Name, Path, Ext string
}
type Builder interface {
Build(ctx *context.Context, build config.Build, options Options) error
}
func Run(ctx *context.Context, target buildtarget.Target, 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)
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Env = append(cmd.Env, env...)
log.Debug("running")
if out, err := cmd.CombinedOutput(); err != nil {
log.WithError(err).Debug("failed")
return errors.New(string(out))
}
return nil
}

30
build/build_test.go Normal file
View File

@ -0,0 +1,30 @@
package build
import (
"testing"
"github.com/goreleaser/goreleaser/build/buildtarget"
"github.com/goreleaser/goreleaser/config"
"github.com/goreleaser/goreleaser/context"
"github.com/stretchr/testify/assert"
)
var emptyEnv []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,
))
}

View File

@ -1,6 +1,6 @@
package ext
import "github.com/goreleaser/goreleaser/internal/buildtarget"
import "github.com/goreleaser/goreleaser/build/buildtarget"
// For returns the binary extension for the given platform
func For(target buildtarget.Target) string {

View File

@ -3,7 +3,7 @@ package ext
import (
"testing"
"github.com/goreleaser/goreleaser/internal/buildtarget"
"github.com/goreleaser/goreleaser/build/buildtarget"
"github.com/stretchr/testify/assert"
)

View File

@ -92,6 +92,7 @@ type Build struct {
Binary string `yaml:",omitempty"`
Hooks Hooks `yaml:",omitempty"`
Env []string `yaml:",omitempty"`
Lang string `yaml:",omitempty"`
// Capture all undefined fields and should be empty after loading
XXX map[string]interface{} `yaml:",inline"`

View File

@ -0,0 +1,132 @@
package golang
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"strings"
"text/template"
"time"
"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"
)
var Default = &Builder{}
func init() {
build.Register("go", Default)
}
type Builder struct {
}
func (*Builder) Build(ctx *context.Context, cfg config.Build, options build.Options) error {
if err := checkMain(ctx, cfg); err != nil {
return err
}
cmd := []string{"go", "build"}
if cfg.Flags != "" {
cmd = append(cmd, strings.Fields(cfg.Flags)...)
}
flags, err := ldflags(ctx, cfg)
if err != nil {
return err
}
cmd = append(cmd, "-ldflags="+flags, "-o", options.Path, cfg.Main)
if err := build.Run(ctx, options.Target, cmd, cfg.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: options.Target.OS,
Goarch: options.Target.Arch,
Goarm: options.Target.Arm,
Extra: map[string]string{
"Binary": cfg.Binary,
"Ext": options.Ext,
},
})
return nil
}
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
}
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,4 +1,4 @@
package build
package golang
import (
"testing"

View File

@ -1,8 +1,7 @@
// Package build needs to be documented
package build
import (
"os"
"os/exec"
"path/filepath"
"strings"
@ -10,11 +9,14 @@ import (
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
builders "github.com/goreleaser/goreleaser/build"
"github.com/goreleaser/goreleaser/build/buildtarget"
"github.com/goreleaser/goreleaser/build/ext"
"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 +30,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,6 +51,9 @@ 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
}
@ -102,7 +104,7 @@ 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 builders.Run(ctx, buildtarget.Runtime, cmd, env)
}
func doBuild(ctx *context.Context, build config.Build, target buildtarget.Target) error {
@ -110,46 +112,10 @@ func doBuild(ctx *context.Context, build config.Build, target buildtarget.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,
return builders.For(build.Lang).Build(ctx, build, builders.Options{
Target: target,
Name: binaryName,
Goos: target.OS,
Goarch: target.Arch,
Goarm: target.Arm,
Extra: map[string]string{
"Binary": build.Binary,
"Ext": ext,
},
Path: binary,
Ext: ext,
})
return nil
}
func run(ctx *context.Context, target buildtarget.Target, 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)
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Env = append(cmd.Env, env...)
log.Debug("running")
if out, err := cmd.CombinedOutput(); err != nil {
log.WithError(err).Debug("failed")
return errors.New(string(out))
}
return nil
}

View File

@ -7,9 +7,9 @@ import (
"runtime"
"testing"
"github.com/goreleaser/goreleaser/build/buildtarget"
"github.com/goreleaser/goreleaser/config"
"github.com/goreleaser/goreleaser/context"
"github.com/goreleaser/goreleaser/internal/buildtarget"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/stretchr/testify/assert"
)
@ -20,24 +20,6 @@ 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{

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
}