1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-10 03:47:03 +02:00
goreleaser/internal/pipe/snapcraft/snapcraft.go

382 lines
11 KiB
Go
Raw Normal View History

2017-07-27 02:30:48 +02:00
// Package snapcraft implements the Pipe interface providing Snapcraft bindings.
package snapcraft
import (
"errors"
2017-08-02 14:06:28 +02:00
"fmt"
2017-07-28 03:05:43 +02:00
"os"
2017-07-27 02:30:48 +02:00
"os/exec"
"path/filepath"
2018-06-25 02:12:53 +02:00
"strings"
2017-12-17 21:25:04 +02:00
2017-07-27 02:30:48 +02:00
"github.com/apex/log"
"gopkg.in/yaml.v2"
2017-12-18 13:00:19 +02:00
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/ids"
2017-08-27 18:18:23 +02:00
"github.com/goreleaser/goreleaser/internal/linux"
2018-09-12 19:18:01 +02:00
"github.com/goreleaser/goreleaser/internal/pipe"
2018-07-10 07:08:22 +02:00
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
2017-07-27 02:30:48 +02:00
)
// ErrNoSnapcraft is shown when snapcraft cannot be found in $PATH.
2017-07-27 02:30:48 +02:00
var ErrNoSnapcraft = errors.New("snapcraft not present in $PATH")
// ErrNoDescription is shown when no description provided.
2017-08-17 23:41:08 +02:00
var ErrNoDescription = errors.New("no description provided for snapcraft")
// ErrNoSummary is shown when no summary provided.
2017-08-17 23:41:08 +02:00
var ErrNoSummary = errors.New("no summary provided for snapcraft")
// Metadata to generate the snap package.
2017-09-15 02:19:56 +02:00
type Metadata struct {
2017-07-27 02:30:48 +02:00
Name string
Version string
Summary string
Description string
Base string `yaml:",omitempty"`
2019-04-17 22:26:30 +02:00
License string `yaml:",omitempty"`
2017-07-27 02:30:48 +02:00
Grade string `yaml:",omitempty"`
Confinement string `yaml:",omitempty"`
Architectures []string
Layout map[string]LayoutMetadata `yaml:",omitempty"`
2017-08-04 07:42:55 +02:00
Apps map[string]AppMetadata
Plugs map[string]interface{} `yaml:",omitempty"`
2017-07-27 02:30:48 +02:00
}
// AppMetadata for the binaries that will be in the snap package.
2017-08-04 07:42:55 +02:00
type AppMetadata struct {
Command string
Plugs []string `yaml:",omitempty"`
Daemon string `yaml:",omitempty"`
Completer string `yaml:",omitempty"`
RestartCondition string `yaml:"restart-condition,omitempty"`
2017-07-27 02:30:48 +02:00
}
type LayoutMetadata struct {
Symlink string `yaml:",omitempty"`
Bind string `yaml:",omitempty"`
BindFile string `yaml:"bind-file,omitempty"`
Type string `yaml:",omitempty"`
}
const defaultNameTemplate = "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
// Pipe for snapcraft packaging.
2017-07-27 02:30:48 +02:00
type Pipe struct{}
func (Pipe) String() string {
return "snapcraft packages"
2017-07-27 02:30:48 +02:00
}
// Default sets the pipe defaults.
func (Pipe) Default(ctx *context.Context) error {
ids := ids.New("snapcrafts")
for i := range ctx.Config.Snapcrafts {
snap := &ctx.Config.Snapcrafts[i]
if snap.NameTemplate == "" {
snap.NameTemplate = defaultNameTemplate
}
if len(snap.Builds) == 0 {
for _, b := range ctx.Config.Builds {
snap.Builds = append(snap.Builds, b.ID)
}
}
ids.Inc(snap.ID)
}
return ids.Validate()
}
// Run the pipe.
2017-07-27 02:30:48 +02:00
func (Pipe) Run(ctx *context.Context) error {
for _, snap := range ctx.Config.Snapcrafts {
// TODO: deal with pipe.skip?
if err := doRun(ctx, snap); err != nil {
return err
}
}
return nil
}
func doRun(ctx *context.Context, snap config.Snapcraft) error {
if snap.Summary == "" && snap.Description == "" {
2018-09-12 19:18:01 +02:00
return pipe.Skip("no summary nor description were provided")
2017-07-27 02:30:48 +02:00
}
if snap.Summary == "" {
2017-08-17 23:41:08 +02:00
return ErrNoSummary
}
if snap.Description == "" {
2017-08-17 23:41:08 +02:00
return ErrNoDescription
2017-07-27 02:30:48 +02:00
}
_, err := exec.LookPath("snapcraft")
if err != nil {
return ErrNoSnapcraft
}
g := semerrgroup.New(ctx.Parallelism)
2017-12-17 21:25:04 +02:00
for platform, binaries := range ctx.Artifacts.Filter(
artifact.And(
artifact.ByGoos("linux"),
artifact.ByType(artifact.Binary),
artifact.ByIDs(snap.Builds...),
2017-12-17 21:25:04 +02:00
),
).GroupByPlatform() {
arch := linux.Arch(platform)
if !isValidArch(arch) {
log.WithField("arch", arch).Warn("ignored unsupported arch")
continue
}
2017-12-17 21:25:04 +02:00
binaries := binaries
g.Go(func() error {
return create(ctx, snap, arch, binaries)
2017-12-17 21:25:04 +02:00
})
2017-07-27 02:30:48 +02:00
}
return g.Wait()
}
func isValidArch(arch string) bool {
// https://snapcraft.io/docs/architectures
for _, a := range []string{"s390x", "ppc64el", "arm64", "armhf", "amd64", "i386"} {
if arch == a {
return true
}
}
return false
}
// Publish packages.
2018-10-20 19:25:46 +02:00
func (Pipe) Publish(ctx *context.Context) error {
if ctx.SkipPublish {
return pipe.ErrSkipPublishEnabled
}
2018-10-20 19:25:46 +02:00
snaps := ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableSnapcraft)).List()
g := semerrgroup.New(ctx.Parallelism)
2018-10-20 19:25:46 +02:00
for _, snap := range snaps {
snap := snap
g.Go(func() error {
2018-10-20 19:26:49 +02:00
return push(ctx, snap)
2018-10-20 19:25:46 +02:00
})
}
return g.Wait()
}
func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries []*artifact.Artifact) error {
log := log.WithField("arch", arch)
folder, err := tmpl.New(ctx).
WithArtifact(binaries[0], snap.Replacements).
Apply(snap.NameTemplate)
2017-12-17 21:25:04 +02:00
if err != nil {
return err
}
2017-07-28 03:05:43 +02:00
// prime is the directory that then will be compressed to make the .snap package.
folderDir := filepath.Join(ctx.Config.Dist, folder)
primeDir := filepath.Join(folderDir, "prime")
metaDir := filepath.Join(primeDir, "meta")
// #nosec
if err = os.MkdirAll(metaDir, 0o755); err != nil {
2017-07-28 03:05:43 +02:00
return err
}
for _, file := range snap.Files {
if file.Destination == "" {
file.Destination = file.Source
}
if file.Mode == 0 {
file.Mode = 0o644
}
if err := os.MkdirAll(filepath.Join(primeDir, filepath.Dir(file.Destination)), 0o755); err != nil {
return fmt.Errorf("failed to link extra file '%s': %w", file.Source, err)
}
if err := link(file.Source, filepath.Join(primeDir, file.Destination), os.FileMode(file.Mode)); err != nil {
return fmt.Errorf("failed to link extra file '%s': %w", file.Source, err)
}
}
file := filepath.Join(primeDir, "meta", "snap.yaml")
log.WithField("file", file).Debug("creating snap metadata")
2017-07-27 02:30:48 +02:00
metadata := &Metadata{
2017-07-27 02:30:48 +02:00
Version: ctx.Version,
Summary: snap.Summary,
Description: snap.Description,
Grade: snap.Grade,
Confinement: snap.Confinement,
2017-07-27 02:30:48 +02:00
Architectures: []string{arch},
Layout: map[string]LayoutMetadata{},
2018-12-12 22:24:22 +02:00
Apps: map[string]AppMetadata{},
2017-07-27 02:30:48 +02:00
}
if snap.Base != "" {
metadata.Base = snap.Base
}
if snap.License != "" {
metadata.License = snap.License
2019-04-17 22:26:30 +02:00
}
metadata.Name = ctx.Config.ProjectName
if snap.Name != "" {
metadata.Name = snap.Name
2017-08-07 18:28:04 +02:00
}
2017-07-27 02:30:48 +02:00
for targetPath, layout := range snap.Layout {
metadata.Layout[targetPath] = LayoutMetadata{
Symlink: layout.Symlink,
Bind: layout.Bind,
BindFile: layout.BindFile,
Type: layout.Type,
}
}
// if the user didn't specify any apps then
// default to the main binary being the command:
if len(snap.Apps) == 0 {
name := snap.Name
if name == "" {
name = filepath.Base(binaries[0].Name)
2017-08-20 21:35:46 +02:00
}
metadata.Apps[name] = AppMetadata{
Command: filepath.Base(filepath.Base(binaries[0].Name)),
2017-08-04 07:42:55 +02:00
}
}
2017-07-28 03:05:43 +02:00
for _, binary := range binaries {
// build the binaries and link resources
2017-07-28 03:05:43 +02:00
destBinaryPath := filepath.Join(primeDir, filepath.Base(binary.Path))
2018-12-13 14:09:36 +02:00
log.WithField("src", binary.Path).
WithField("dst", destBinaryPath).
Debug("linking")
2017-12-18 02:31:27 +02:00
if err = os.Link(binary.Path, destBinaryPath); err != nil {
return fmt.Errorf("failed to link binary: %w", err)
2018-12-12 22:32:22 +02:00
}
if err := os.Chmod(destBinaryPath, 0o555); err != nil {
return fmt.Errorf("failed to change binary permissions: %w", err)
2017-08-02 14:06:28 +02:00
}
}
// setup the apps: directive for each binary
for name, config := range snap.Apps {
command := name
if config.Command != "" {
command = config.Command
}
// TODO: test that the correct binary is used in Command
// See https://github.com/goreleaser/goreleaser/pull/1449
appMetadata := AppMetadata{
Command: strings.TrimSpace(strings.Join([]string{
command,
config.Args,
}, " ")),
Plugs: config.Plugs,
Daemon: config.Daemon,
RestartCondition: config.RestartCondition,
}
if config.Completer != "" {
destCompleterPath := filepath.Join(primeDir, config.Completer)
if err := os.MkdirAll(filepath.Dir(destCompleterPath), 0o755); err != nil {
return fmt.Errorf("failed to create folder: %w", err)
}
log.WithField("src", config.Completer).
WithField("dst", destCompleterPath).
Debug("linking")
if err := os.Link(config.Completer, destCompleterPath); err != nil {
return fmt.Errorf("failed to link completer: %w", err)
}
if err := os.Chmod(destCompleterPath, 0o644); err != nil {
return fmt.Errorf("failed to change completer permissions: %w", err)
}
appMetadata.Completer = config.Completer
}
metadata.Apps[name] = appMetadata
metadata.Plugs = snap.Plugs
}
2017-07-27 02:30:48 +02:00
out, err := yaml.Marshal(metadata)
if err != nil {
return err
}
2019-03-20 02:45:08 +02:00
log.WithField("file", file).Debugf("writing metadata file")
if err = os.WriteFile(file, out, 0o644); err != nil { //nolint: gosec
2017-07-27 02:30:48 +02:00
return err
}
snapFile := filepath.Join(ctx.Config.Dist, folder+".snap")
log.WithField("snap", snapFile).Info("creating")
/* #nosec */
cmd := exec.CommandContext(ctx, "snapcraft", "pack", primeDir, "--output", snapFile)
2017-07-27 05:43:53 +02:00
if out, err = cmd.CombinedOutput(); err != nil {
2017-08-02 14:06:28 +02:00
return fmt.Errorf("failed to generate snap package: %s", string(out))
2017-07-27 05:43:53 +02:00
}
if !snap.Publish {
2018-10-20 20:13:31 +02:00
return nil
}
ctx.Artifacts.Add(&artifact.Artifact{
2018-10-20 19:25:46 +02:00
Type: artifact.PublishableSnapcraft,
2017-12-17 21:25:04 +02:00
Name: folder + ".snap",
Path: snapFile,
2017-12-17 21:25:04 +02:00
Goos: binaries[0].Goos,
Goarch: binaries[0].Goarch,
Goarm: binaries[0].Goarm,
})
2017-07-27 02:30:48 +02:00
return nil
}
2018-10-20 19:26:49 +02:00
const (
reviewWaitMsg = `Waiting for previous upload(s) to complete their review process.`
humanReviewMsg = `A human will soon review your snap`
needsReviewMsg = `(NEEDS REVIEW)`
)
func push(ctx *context.Context, snap *artifact.Artifact) error {
log := log.WithField("snap", snap.Name)
log.Info("pushing snap")
2018-10-20 22:47:55 +02:00
// TODO: customize --release based on snap.Grade?
2018-10-20 20:16:14 +02:00
/* #nosec */
cmd := exec.CommandContext(ctx, "snapcraft", "upload", "--release=stable", snap.Path)
2018-10-20 19:26:49 +02:00
if out, err := cmd.CombinedOutput(); err != nil {
if strings.Contains(string(out), reviewWaitMsg) || strings.Contains(string(out), humanReviewMsg) || strings.Contains(string(out), needsReviewMsg) {
log.Warn(reviewWaitMsg)
} else {
return fmt.Errorf("failed to push %s package: %s", snap.Path, string(out))
}
2018-10-20 19:26:49 +02:00
}
snap.Type = artifact.Snapcraft
ctx.Artifacts.Add(snap)
return nil
}
// walks the src, recreating dirs and hard-linking files.
func link(src, dest string, mode os.FileMode) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// We have the following:
// - src = "a/b"
// - dest = "dist/linuxamd64/b"
// - path = "a/b/c.txt"
// So we join "a/b" with "c.txt" and use it as the destination.
dst := filepath.Join(dest, strings.Replace(path, src, "", 1))
log.WithFields(log.Fields{
"src": path,
"dst": dst,
}).Debug("extra file")
if info.IsDir() {
return os.MkdirAll(dst, info.Mode())
}
if err := os.Link(path, dst); err != nil {
return err
}
return os.Chmod(dst, mode)
})
}