diff --git a/.travis.yml b/.travis.yml index b59a70ef6..719bef157 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,9 @@ install: make setup script: - make ci after_success: - - git status + - test -n "$TRAVIS_TAG" && gem install fpm + - git status -sb - test -n "$TRAVIS_TAG" && go run main.go + - git status -sb notifications: email: false diff --git a/README.md b/README.md index a7dc072f6..02431c9cf 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,14 @@ GoReleaser uses the latest [Git tag](https://git-scm.com/book/en/v2/Git-Basics-T Create a tag: ```console -$ git tag -a v0.1 -m "First release" +$ git tag -a v0.1.0 -m "First release" ``` +**Note**: we recommend the use of [semantic versioning](http://semver.org/). We +are not enforcing it though. We do remove the `v` prefix and then enforce +that the next character is a number. So, `v0.1.0` and `0.1.0` are virtually the +same and are both accepted, while `version0.1.0` is not. + Now you can run GoReleaser at the root of your repository: ```console @@ -285,6 +290,28 @@ class Program < Formula end ``` +### FPM build customization + +GoReleaser can be wired to [fpm]() to generate `.deb`, `.rpm` and other archives. Check it's +[wiki](https://github.com/jordansissel/fpm/wiki) for more info. + +[fpm]: https://github.com/jordansissel/fpm + +```yml +# goreleaser.yml +fpm: + # Formats to generate as output + formats: + - deb + - rpm + + # Dependencies of your package + dependencies: + - git +``` + +Note that GoReleaser will not install `fpm` nor any of it's dependencies for you. + ## Integration with CI You may want to wire this to auto-deploy your new tags on [Travis](https://travis-ci.org), for example: diff --git a/config/config.go b/config/config.go index af5489d41..43563482a 100644 --- a/config/config.go +++ b/config/config.go @@ -43,12 +43,19 @@ type Release struct { Repo string } +// FPM config +type FPM struct { + Formats []string + Dependencies []string +} + // Project includes all project configuration type Project struct { Release Release Brew Homebrew Build Build Archive Archive + FPM FPM `yaml:"fpm"` } // Load config file diff --git a/context/context.go b/context/context.go index a474287f1..68ce61571 100644 --- a/context/context.go +++ b/context/context.go @@ -22,6 +22,7 @@ type Context struct { ReleaseRepo Repo BrewRepo Repo Archives map[string]string + Version string } // New context diff --git a/goreleaser.yml b/goreleaser.yml index 2da2612d2..1f7d62dae 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -3,3 +3,8 @@ brew: folder: Formula dependencies: - git +fpm: + formats: + - deb + dependencies: + - git diff --git a/main.go b/main.go index 7c7888690..ea77d02bc 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "github.com/goreleaser/goreleaser/pipeline/build" "github.com/goreleaser/goreleaser/pipeline/defaults" "github.com/goreleaser/goreleaser/pipeline/env" + "github.com/goreleaser/goreleaser/pipeline/fpm" "github.com/goreleaser/goreleaser/pipeline/git" "github.com/goreleaser/goreleaser/pipeline/release" "github.com/goreleaser/goreleaser/pipeline/repos" @@ -33,6 +34,7 @@ var pipes = []pipeline.Pipe{ // real work build.Pipe{}, archive.Pipe{}, + fpm.Pipe{}, release.Pipe{}, brew.Pipe{}, } diff --git a/pipeline/brew/brew.go b/pipeline/brew/brew.go index aa03669a7..2236fe256 100644 --- a/pipeline/brew/brew.go +++ b/pipeline/brew/brew.go @@ -22,7 +22,7 @@ const formula = `class {{ .Name }} < Formula desc "{{ .Desc }}" homepage "{{ .Homepage }}" url "https://github.com/{{ .Repo }}/releases/download/{{ .Tag }}/{{ .File }}.{{ .Format }}" - version "{{ .Tag }}" + version "{{ .Version }}" sha256 "{{ .SHA256 }}" {{- if .Dependencies }} @@ -50,6 +50,7 @@ type templateData struct { Homepage string Repo string Tag string + Version string BinaryName string Caveats string File string @@ -156,6 +157,7 @@ func dataFor(ctx *context.Context, client *github.Client) (result templateData, Homepage: homepage, Repo: ctx.Config.Release.Repo, Tag: ctx.Git.CurrentTag, + Version: ctx.Version, BinaryName: ctx.Config.Build.BinaryName, Caveats: ctx.Config.Brew.Caveats, File: file, diff --git a/pipeline/brew/brew_test.go b/pipeline/brew/brew_test.go index f9dca51f8..e9ada50af 100644 --- a/pipeline/brew/brew_test.go +++ b/pipeline/brew/brew_test.go @@ -25,6 +25,7 @@ var defaultTemplateData = templateData{ Name: "Test", Repo: "caarlos0/test", Tag: "v0.1.3", + Version: "0.1.3", File: "test_Darwin_x86_64", SHA256: "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c68", Format: "tar.gz", @@ -36,7 +37,7 @@ func assertDefaultTemplateData(t *testing.T, formulae string) { assert.Contains(formulae, "homepage \"https://google.com\"") assert.Contains(formulae, "url \"https://github.com/caarlos0/test/releases/download/v0.1.3/test_Darwin_x86_64.tar.gz\"") assert.Contains(formulae, "sha256 \"1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c68\"") - assert.Contains(formulae, "version \"v0.1.3\"") + assert.Contains(formulae, "version \"0.1.3\"") assert.Contains(formulae, "bin.install \"test\"") } diff --git a/pipeline/build/build.go b/pipeline/build/build.go index 2057abbcf..9b3f7bd77 100644 --- a/pipeline/build/build.go +++ b/pipeline/build/build.go @@ -1,7 +1,6 @@ package build import ( - "bytes" "errors" "log" "os" @@ -41,7 +40,7 @@ func (Pipe) Run(ctx *context.Context) error { } func build(name, goos, goarch string, ctx *context.Context) error { - ldflags := ctx.Config.Build.Ldflags + " -X main.version=" + ctx.Git.CurrentTag + ldflags := ctx.Config.Build.Ldflags + " -X main.version=" + ctx.Version output := "dist/" + name + "/" + ctx.Config.Build.BinaryName + extFor(goos) log.Println("Building", output) if ctx.Config.Build.Hooks.Pre != "" { @@ -67,11 +66,8 @@ func run(goos, goarch string, command []string) error { cmd := exec.Command(command[0], command[1:]...) cmd.Env = append(cmd.Env, os.Environ()...) cmd.Env = append(cmd.Env, "GOOS="+goos, "GOARCH="+goarch) - var stdout bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stdout - if err := cmd.Run(); err != nil { - return errors.New(stdout.String()) + if out, err := cmd.CombinedOutput(); err != nil { + return errors.New(string(out)) } return nil } diff --git a/pipeline/fpm/fpm.go b/pipeline/fpm/fpm.go new file mode 100644 index 000000000..7e2681a65 --- /dev/null +++ b/pipeline/fpm/fpm.go @@ -0,0 +1,84 @@ +package fpm + +import ( + "errors" + "log" + "os/exec" + "path/filepath" + + "github.com/goreleaser/goreleaser/context" + "golang.org/x/sync/errgroup" +) + +var goarchToUnix = map[string]string{ + "386": "i386", + "amd64": "x86_64", +} + +// ErrNoFPM is shown when fpm cannot be found in $PATH +var ErrNoFPM = errors.New("fpm not present in $PATH") + +// Pipe for fpm packaging +type Pipe struct{} + +// Description of the pipe +func (Pipe) Description() string { + return "Creating Linux packages with fpm" +} + +// Run the pipe +func (Pipe) Run(ctx *context.Context) error { + if len(ctx.Config.FPM.Formats) == 0 { + log.Println("No output formats configured, skipping") + return nil + } + _, err := exec.LookPath("fpm") + if err != nil { + return ErrNoFPM + } + + var g errgroup.Group + for _, format := range ctx.Config.FPM.Formats { + for _, goarch := range ctx.Config.Build.Goarch { + if ctx.Archives["linux"+goarch] == "" { + continue + } + archive := ctx.Archives["linux"+goarch] + arch := goarchToUnix[goarch] + g.Go(func() error { + return create(ctx, format, archive, arch) + }) + } + } + return g.Wait() +} + +func create(ctx *context.Context, format, archive, arch string) error { + var path = filepath.Join("dist", archive) + var file = path + ".deb" + var name = ctx.Config.Build.BinaryName + log.Println("Creating", file) + + var options = []string{ + "-s", "dir", + "-t", format, + "-n", name, + "-v", ctx.Version, + "-a", arch, + "-C", path, + "-p", file, + "--force", + } + for _, dep := range ctx.Config.FPM.Dependencies { + options = append(options, "-d", dep) + } + // This basically tells fpm to put the binary in the /usr/local/bin + // binary=/usr/local/bin/binary + options = append(options, name+"="+filepath.Join("/usr/local/bin", name)) + cmd := exec.Command("fpm", options...) + + if out, err := cmd.CombinedOutput(); err != nil { + return errors.New(string(out)) + } + return nil +} diff --git a/pipeline/git/git.go b/pipeline/git/git.go index 37b2d735a..cdd62e67a 100644 --- a/pipeline/git/git.go +++ b/pipeline/git/git.go @@ -1,6 +1,20 @@ package git -import "github.com/goreleaser/goreleaser/context" +import ( + "regexp" + "strings" + + "github.com/goreleaser/goreleaser/context" +) + +// ErrInvalidVersionFormat is return when the version isnt in a valid format +type ErrInvalidVersionFormat struct { + version string +} + +func (e ErrInvalidVersionFormat) Error() string { + return e.version + " is not in a valid version format" +} // Pipe for brew deployment type Pipe struct{} @@ -30,5 +44,10 @@ func (Pipe) Run(ctx *context.Context) (err error) { PreviousTag: previous, Diff: log, } + // removes usual `v` prefix + ctx.Version = strings.TrimPrefix(tag, "v") + if matches, err := regexp.MatchString("^[0-9.]+", ctx.Version); !matches || err != nil { + return ErrInvalidVersionFormat{ctx.Version} + } return } diff --git a/pipeline/release/release.go b/pipeline/release/release.go index 6acea0ce4..4efc050e9 100644 --- a/pipeline/release/release.go +++ b/pipeline/release/release.go @@ -4,6 +4,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "github.com/google/go-github/github" "github.com/goreleaser/goreleaser/clients" @@ -31,9 +32,14 @@ func (Pipe) Run(ctx *context.Context) error { for _, archive := range ctx.Archives { archive := archive g.Go(func() error { - return upload(client, *r.ID, archive, ctx) + return upload(ctx, client, *r.ID, archive, ctx.Config.Archive.Format) }) - + for _, format := range ctx.Config.FPM.Formats { + format := format + g.Go(func() error { + return upload(ctx, client, *r.ID, archive, format) + }) + } } return g.Wait() } @@ -67,9 +73,20 @@ func description(diff string) string { return result + "\nBuilt with " + string(bts) } -func upload(client *github.Client, releaseID int, archive string, ctx *context.Context) error { - archive = archive + "." + ctx.Config.Archive.Format - file, err := os.Open("dist/" + archive) +func upload(ctx *context.Context, client *github.Client, releaseID int, archive, format string) error { + archive = archive + "." + format + var path = filepath.Join("dist", archive) + // In case the file doesn't exist, we just ignore it. + // We do this because we can get invalid combinations of archive+format here, + // like darwinamd64 + deb or something like that. + // It's assumed that the archive pipe would fail the entire thing in case it fails to + // generate some archive, as well fpm pipe is expected to fail if something wrong happens. + // So, here, we just assume IsNotExist as an expected error. + // TODO: maybe add a list of files to upload in the context so we don't have to do this. + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil + } + file, err := os.Open(path) if err != nil { return err }