github.com/wesleimp/goreleaser@v0.92.0/internal/pipe/snapcraft/snapcraft.go (about)

     1  // Package snapcraft implements the Pipe interface providing Snapcraft bindings.
     2  package snapcraft
     3  
     4  import (
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/apex/log"
    14  	"github.com/goreleaser/goreleaser/internal/artifact"
    15  	"github.com/goreleaser/goreleaser/internal/linux"
    16  	"github.com/goreleaser/goreleaser/internal/pipe"
    17  	"github.com/goreleaser/goreleaser/internal/semerrgroup"
    18  	"github.com/goreleaser/goreleaser/internal/tmpl"
    19  	"github.com/goreleaser/goreleaser/pkg/context"
    20  	yaml "gopkg.in/yaml.v2"
    21  )
    22  
    23  // ErrNoSnapcraft is shown when snapcraft cannot be found in $PATH
    24  var ErrNoSnapcraft = errors.New("snapcraft not present in $PATH")
    25  
    26  // ErrNoDescription is shown when no description provided
    27  var ErrNoDescription = errors.New("no description provided for snapcraft")
    28  
    29  // ErrNoSummary is shown when no summary provided
    30  var ErrNoSummary = errors.New("no summary provided for snapcraft")
    31  
    32  // Metadata to generate the snap package
    33  type Metadata struct {
    34  	Name          string
    35  	Version       string
    36  	Summary       string
    37  	Description   string
    38  	Grade         string `yaml:",omitempty"`
    39  	Confinement   string `yaml:",omitempty"`
    40  	Architectures []string
    41  	Apps          map[string]AppMetadata
    42  }
    43  
    44  // AppMetadata for the binaries that will be in the snap package
    45  type AppMetadata struct {
    46  	Command string
    47  	Plugs   []string `yaml:",omitempty"`
    48  	Daemon  string   `yaml:",omitempty"`
    49  }
    50  
    51  const defaultNameTemplate = "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
    52  
    53  // Pipe for snapcraft packaging
    54  type Pipe struct{}
    55  
    56  func (Pipe) String() string {
    57  	return "Snapcraft Packages"
    58  }
    59  
    60  // Default sets the pipe defaults
    61  func (Pipe) Default(ctx *context.Context) error {
    62  	var snap = &ctx.Config.Snapcraft
    63  	if snap.NameTemplate == "" {
    64  		snap.NameTemplate = defaultNameTemplate
    65  	}
    66  	return nil
    67  }
    68  
    69  // Run the pipe
    70  func (Pipe) Run(ctx *context.Context) error {
    71  	if ctx.Config.Snapcraft.Summary == "" && ctx.Config.Snapcraft.Description == "" {
    72  		return pipe.Skip("no summary nor description were provided")
    73  	}
    74  	if ctx.Config.Snapcraft.Summary == "" {
    75  		return ErrNoSummary
    76  	}
    77  	if ctx.Config.Snapcraft.Description == "" {
    78  		return ErrNoDescription
    79  	}
    80  	_, err := exec.LookPath("snapcraft")
    81  	if err != nil {
    82  		return ErrNoSnapcraft
    83  	}
    84  
    85  	var g = semerrgroup.New(ctx.Parallelism)
    86  	for platform, binaries := range ctx.Artifacts.Filter(
    87  		artifact.And(
    88  			artifact.ByGoos("linux"),
    89  			artifact.ByType(artifact.Binary),
    90  		),
    91  	).GroupByPlatform() {
    92  		arch := linux.Arch(platform)
    93  		if arch == "armel" {
    94  			log.WithField("arch", arch).Warn("ignored unsupported arch")
    95  			continue
    96  		}
    97  		binaries := binaries
    98  		g.Go(func() error {
    99  			return create(ctx, arch, binaries)
   100  		})
   101  	}
   102  	return g.Wait()
   103  }
   104  
   105  // Publish packages
   106  func (Pipe) Publish(ctx *context.Context) error {
   107  	snaps := ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableSnapcraft)).List()
   108  	var g = semerrgroup.New(ctx.Parallelism)
   109  	for _, snap := range snaps {
   110  		snap := snap
   111  		g.Go(func() error {
   112  			return push(ctx, snap)
   113  		})
   114  	}
   115  	return g.Wait()
   116  }
   117  
   118  func create(ctx *context.Context, arch string, binaries []artifact.Artifact) error {
   119  	var log = log.WithField("arch", arch)
   120  	folder, err := tmpl.New(ctx).
   121  		WithArtifact(binaries[0], ctx.Config.Snapcraft.Replacements).
   122  		Apply(ctx.Config.Snapcraft.NameTemplate)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	// prime is the directory that then will be compressed to make the .snap package.
   127  	var folderDir = filepath.Join(ctx.Config.Dist, folder)
   128  	var primeDir = filepath.Join(folderDir, "prime")
   129  	var metaDir = filepath.Join(primeDir, "meta")
   130  	// #nosec
   131  	if err = os.MkdirAll(metaDir, 0755); err != nil {
   132  		return err
   133  	}
   134  
   135  	var file = filepath.Join(primeDir, "meta", "snap.yaml")
   136  	log.WithField("file", file).Debug("creating snap metadata")
   137  
   138  	var metadata = &Metadata{
   139  		Version:       ctx.Version,
   140  		Summary:       ctx.Config.Snapcraft.Summary,
   141  		Description:   ctx.Config.Snapcraft.Description,
   142  		Grade:         ctx.Config.Snapcraft.Grade,
   143  		Confinement:   ctx.Config.Snapcraft.Confinement,
   144  		Architectures: []string{arch},
   145  		Apps:          make(map[string]AppMetadata),
   146  	}
   147  
   148  	metadata.Name = ctx.Config.ProjectName
   149  	if ctx.Config.Snapcraft.Name != "" {
   150  		metadata.Name = ctx.Config.Snapcraft.Name
   151  	}
   152  
   153  	for _, binary := range binaries {
   154  		log.WithField("path", binary.Path).
   155  			WithField("name", binary.Name).
   156  			Debug("passed binary to snapcraft")
   157  		appMetadata := AppMetadata{
   158  			Command: binary.Name,
   159  		}
   160  		if configAppMetadata, ok := ctx.Config.Snapcraft.Apps[binary.Name]; ok {
   161  			appMetadata.Plugs = configAppMetadata.Plugs
   162  			appMetadata.Daemon = configAppMetadata.Daemon
   163  			appMetadata.Command = strings.Join([]string{
   164  				appMetadata.Command,
   165  				configAppMetadata.Args,
   166  			}, " ")
   167  		}
   168  		metadata.Apps[binary.Name] = appMetadata
   169  
   170  		destBinaryPath := filepath.Join(primeDir, filepath.Base(binary.Path))
   171  		if err = os.Link(binary.Path, destBinaryPath); err != nil {
   172  			return err
   173  		}
   174  	}
   175  
   176  	if _, ok := metadata.Apps[metadata.Name]; !ok {
   177  		metadata.Apps[metadata.Name] = metadata.Apps[binaries[0].Name]
   178  	}
   179  
   180  	out, err := yaml.Marshal(metadata)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	if err = ioutil.WriteFile(file, out, 0644); err != nil {
   186  		return err
   187  	}
   188  
   189  	var snap = filepath.Join(ctx.Config.Dist, folder+".snap")
   190  	log.WithField("snap", snap).Info("creating")
   191  	/* #nosec */
   192  	var cmd = exec.CommandContext(ctx, "snapcraft", "pack", primeDir, "--output", snap)
   193  	if out, err = cmd.CombinedOutput(); err != nil {
   194  		return fmt.Errorf("failed to generate snap package: %s", string(out))
   195  	}
   196  	if !ctx.Config.Snapcraft.Publish {
   197  		return nil
   198  	}
   199  	ctx.Artifacts.Add(artifact.Artifact{
   200  		Type:   artifact.PublishableSnapcraft,
   201  		Name:   folder + ".snap",
   202  		Path:   snap,
   203  		Goos:   binaries[0].Goos,
   204  		Goarch: binaries[0].Goarch,
   205  		Goarm:  binaries[0].Goarm,
   206  	})
   207  	return nil
   208  }
   209  
   210  func push(ctx *context.Context, snap artifact.Artifact) error {
   211  	log.WithField("snap", snap.Name).Info("pushing snap")
   212  	// TODO: customize --release based on snap.Grade?
   213  	/* #nosec */
   214  	var cmd = exec.CommandContext(ctx, "snapcraft", "push", "--release=stable", snap.Path)
   215  	if out, err := cmd.CombinedOutput(); err != nil {
   216  		return fmt.Errorf("failed to push %s package: %s", snap.Path, string(out))
   217  	}
   218  	snap.Type = artifact.Snapcraft
   219  	ctx.Artifacts.Add(snap)
   220  	return nil
   221  }