From e8bf270ba27e53d89a43c86842df0f041e8e1528 Mon Sep 17 00:00:00 2001 From: Leo Arias Date: Thu, 27 Jul 2017 00:30:48 +0000 Subject: [PATCH] add the snapcraft pipeline --- .goreleaser.yml | 8 ++ config/config.go | 27 +++-- goreleaserlib/goreleaser.go | 2 + pipeline/snapcraft/snapcraft.go | 146 +++++++++++++++++++++++++++ pipeline/snapcraft/snapcraft_test.go | 85 ++++++++++++++++ 5 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 pipeline/snapcraft/snapcraft.go create mode 100644 pipeline/snapcraft/snapcraft_test.go diff --git a/.goreleaser.yml b/.goreleaser.yml index f1fe588f8..05a106c47 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -41,3 +41,11 @@ fpm: - deb dependencies: - git +snapcraft: + summary: Deliver Go binaries as fast and easily as possible + description: | + GoReleaser builds Go binaries for several platforms, creates a GitHub + release and then pushes a Homebrew formula to a repository. All that + wrapped in your favorite CI. + grade: stable + confinement: classic diff --git a/config/config.go b/config/config.go index 81f8030e8..65c14d78c 100644 --- a/config/config.go +++ b/config/config.go @@ -123,6 +123,17 @@ type FPM struct { XXX map[string]interface{} `yaml:",inline"` } +// Snapcraft config +type Snapcraft struct { + Summary string `yaml:",omitempty"` + Description string `yaml:",omitempty"` + Grade string `yaml:",omitempty"` + Confinement string `yaml:",omitempty"` + + // Capture all undefined fields and should be empty after loading + XXX map[string]interface{} `yaml:",inline"` +} + // Snapshot config type Snapshot struct { NameTemplate string `yaml:"name_template,omitempty"` @@ -133,13 +144,14 @@ type Snapshot struct { // Project includes all project configuration type Project struct { - ProjectName string `yaml:"project_name,omitempty"` - Release Release `yaml:",omitempty"` - Brew Homebrew `yaml:",omitempty"` - Builds []Build `yaml:",omitempty"` - Archive Archive `yaml:",omitempty"` - FPM FPM `yaml:",omitempty"` - Snapshot Snapshot `yaml:",omitempty"` + ProjectName string `yaml:"project_name,omitempty"` + Release Release `yaml:",omitempty"` + Brew Homebrew `yaml:",omitempty"` + Builds []Build `yaml:",omitempty"` + Archive Archive `yaml:",omitempty"` + FPM FPM `yaml:",omitempty"` + Snapcraft Snapcraft `yaml:",omitempty"` + Snapshot Snapshot `yaml:",omitempty"` // this is a hack ¯\_(ツ)_/¯ SingleBuild Build `yaml:"build,omitempty"` @@ -191,6 +203,7 @@ func checkOverflows(config Project) error { } } checker.check(config.FPM.XXX, "fpm") + checker.check(config.Snapcraft.XXX, "snapcraft") checker.check(config.Release.XXX, "release") checker.check(config.Release.GitHub.XXX, "release.github") checker.check(config.SingleBuild.XXX, "build") diff --git a/goreleaserlib/goreleaser.go b/goreleaserlib/goreleaser.go index 48ec84be7..49055706b 100644 --- a/goreleaserlib/goreleaser.go +++ b/goreleaserlib/goreleaser.go @@ -20,6 +20,7 @@ import ( "github.com/goreleaser/goreleaser/pipeline/fpm" "github.com/goreleaser/goreleaser/pipeline/git" "github.com/goreleaser/goreleaser/pipeline/release" + "github.com/goreleaser/goreleaser/pipeline/snapcraft" yaml "gopkg.in/yaml.v2" ) @@ -31,6 +32,7 @@ var pipes = []pipeline.Pipe{ build.Pipe{}, // build archive.Pipe{}, // archive (tar.gz, zip, etc) fpm.Pipe{}, // archive via fpm (deb, rpm, etc) + snapcraft.Pipe{}, // archive via snapcraft (snap) checksums.Pipe{}, // checksums of the files release.Pipe{}, // release to github brew.Pipe{}, // push to brew tap diff --git a/pipeline/snapcraft/snapcraft.go b/pipeline/snapcraft/snapcraft.go new file mode 100644 index 000000000..40ea907bf --- /dev/null +++ b/pipeline/snapcraft/snapcraft.go @@ -0,0 +1,146 @@ +// Package snapcraft implements the Pipe interface providing Snapcraft bindings. +package snapcraft + +import ( + "errors" + "io/ioutil" + "os/exec" + "path/filepath" + "strings" + + "github.com/apex/log" + "github.com/goreleaser/goreleaser/context" + "golang.org/x/sync/errgroup" + yaml "gopkg.in/yaml.v2" +) + +// ErrNoSnapcraft is shown when snapcraft cannot be found in $PATH +var ErrNoSnapcraft = errors.New("snapcraft not present in $PATH") + +// SnapcraftMetadata to generate the snap package +type SnapcraftMetadata struct { + Name string + Version string + Summary string + Description string + Grade string `yaml:",omitempty"` + Confinement string `yaml:",omitempty"` + Architectures []string + Apps map[string]AppsMetadata + Parts map[string]PartsMetadata +} + +// AppsMetadata for the binaries that will be in the snap package +type AppsMetadata struct { + Command string + // Plugs []string + // Daemon string +} + +// PartsMetadata for the binaries that will be in the snap package +type PartsMetadata struct { + Source string + Plugin string + Prime []string +} + +// Pipe for snapcraft packaging +type Pipe struct{} + +// Description of the pipe +func (Pipe) Description() string { + return "Creating Linux packages with snapcraft" +} + +// Run the pipe +func (Pipe) Run(ctx *context.Context) error { + if ctx.Config.Snapcraft.Summary == "" { + log.Info("no snapcraft summary defined, skipping") + return nil + } + if ctx.Config.Snapcraft.Summary == "" { + log.Info("no snapcraft description defined, skipping") + return nil + } + _, err := exec.LookPath("snapcraft") + if err != nil { + return ErrNoSnapcraft + } + + var g errgroup.Group + for platform, groups := range ctx.Binaries { + if !strings.Contains(platform, "linux") { + log.WithField("platform", platform).Debug("skipped non-linux builds for snapcraft") + continue + } + arch := archFor(platform) + for folder, binaries := range groups { + g.Go(func() error { + return create(ctx, folder, arch, binaries) + }) + } + } + return g.Wait() +} + +func archFor(key string) string { + switch { + case strings.Contains(key, "amd64"): + return "amd64" + case strings.Contains(key, "386"): + return "i386" + case strings.Contains(key, "arm64"): + return "arm64" + case strings.Contains(key, "arm6"): + return "armhf" + } + return key +} + +func create(ctx *context.Context, folder, arch string, binaries []context.Binary) error { + var path = filepath.Join(ctx.Config.Dist, folder) + var file = filepath.Join(path, "snapcraft.yaml") + log.WithField("file", file).Info("creating snapcraft metadata") + + var metadata = &SnapcraftMetadata{ + Name: ctx.Config.ProjectName, + Version: ctx.Version, + Summary: ctx.Config.Snapcraft.Summary, + Description: ctx.Config.Snapcraft.Description, + Grade: ctx.Config.Snapcraft.Grade, + Confinement: ctx.Config.Snapcraft.Confinement, + Architectures: []string{arch}, + Apps: make(map[string]AppsMetadata), + Parts: make(map[string]PartsMetadata), + } + + metadata.Parts[ctx.Config.ProjectName] = PartsMetadata{ + Source: ".", + Plugin: "dump", + } + for _, binary := range binaries { + log.WithField("path", binary.Path). + WithField("name", binary.Name). + Info("passed binary to snapcraft") + metadata.Apps[binary.Name] = AppsMetadata{Command: binary.Name} + prime := metadata.Parts[ctx.Config.ProjectName].Prime + prime = append(prime, binary.Path) + } + out, err := yaml.Marshal(metadata) + if err != nil { + return err + } + + if err = ioutil.WriteFile(file, out, 0644); err != nil { + return err + } + + snap := metadata.Name + "_" + metadata.Version + "_" + arch + ".snap" + cmd := exec.Command("snapcraft", "snap", "--output", snap) + cmd.Dir = path + if out, err := cmd.CombinedOutput(); err != nil { + return errors.New(string(out)) + } + ctx.AddArtifact(filepath.Join(path, snap)) + return nil +} diff --git a/pipeline/snapcraft/snapcraft_test.go b/pipeline/snapcraft/snapcraft_test.go new file mode 100644 index 000000000..b434b1769 --- /dev/null +++ b/pipeline/snapcraft/snapcraft_test.go @@ -0,0 +1,85 @@ +package snapcraft + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/goreleaser/goreleaser/config" + "github.com/goreleaser/goreleaser/context" + "github.com/stretchr/testify/assert" +) + +func TestDescription(t *testing.T) { + assert.NotEmpty(t, Pipe{}.Description()) +} + +func TestRunPipeNoSummary(t *testing.T) { + var assert = assert.New(t) + var ctx = &context.Context{ + Config: config.Project{ + Snapcraft: config.Snapcraft{ + Description: "dummy", + }, + }, + } + assert.NoError(Pipe{}.Run(ctx)) +} + +func TestRunPipeNoDescription(t *testing.T) { + var assert = assert.New(t) + var ctx = &context.Context{ + Config: config.Project{ + Snapcraft: config.Snapcraft{ + Summary: "dummy", + }, + }, + } + assert.NoError(Pipe{}.Run(ctx)) +} + +func TestRunPipe(t *testing.T) { + var assert = assert.New(t) + folder, err := ioutil.TempDir("", "archivetest") + assert.NoError(err) + var dist = filepath.Join(folder, "dist") + assert.NoError(os.Mkdir(dist, 0755)) + assert.NoError(os.Mkdir(filepath.Join(dist, "mybin"), 0755)) + var binPath = filepath.Join(dist, "mybin", "mybin") + _, err = os.Create(binPath) + assert.NoError(err) + var ctx = &context.Context{ + Version: "testversion", + Config: config.Project{ + ProjectName: "mybin", + Dist: dist, + Snapcraft: config.Snapcraft{ + Summary: "test summary", + Description: "test description", + }, + }, + } + for _, plat := range []string{"linuxamd64", "linux386", "darwinamd64"} { + ctx.AddBinary(plat, "mybin", "mybin", binPath) + } + assert.NoError(Pipe{}.Run(ctx)) +} + +func TestNoSnapcraftInPath(t *testing.T) { + var assert = assert.New(t) + var path = os.Getenv("PATH") + defer func() { + assert.NoError(os.Setenv("PATH", path)) + }() + assert.NoError(os.Setenv("PATH", "")) + var ctx = &context.Context{ + Config: config.Project{ + Snapcraft: config.Snapcraft{ + Summary: "dummy", + Description: "dummy", + }, + }, + } + assert.EqualError(Pipe{}.Run(ctx), ErrNoSnapcraft.Error()) +}