github.com/ahmet2mir/goreleaser@v0.180.3-0.20210927151101-8e5ee5a9b8c5/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 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 12 "github.com/apex/log" 13 "gopkg.in/yaml.v2" 14 15 "github.com/goreleaser/goreleaser/internal/artifact" 16 "github.com/goreleaser/goreleaser/internal/gio" 17 "github.com/goreleaser/goreleaser/internal/ids" 18 "github.com/goreleaser/goreleaser/internal/linux" 19 "github.com/goreleaser/goreleaser/internal/pipe" 20 "github.com/goreleaser/goreleaser/internal/semerrgroup" 21 "github.com/goreleaser/goreleaser/internal/tmpl" 22 "github.com/goreleaser/goreleaser/pkg/config" 23 "github.com/goreleaser/goreleaser/pkg/context" 24 ) 25 26 const releasesExtra = "releases" 27 28 // ErrNoSnapcraft is shown when snapcraft cannot be found in $PATH. 29 var ErrNoSnapcraft = errors.New("snapcraft not present in $PATH") 30 31 // ErrNoDescription is shown when no description provided. 32 var ErrNoDescription = errors.New("no description provided for snapcraft") 33 34 // ErrNoSummary is shown when no summary provided. 35 var ErrNoSummary = errors.New("no summary provided for snapcraft") 36 37 // Metadata to generate the snap package. 38 type Metadata struct { 39 Name string 40 Version string 41 Summary string 42 Description string 43 Base string `yaml:",omitempty"` 44 License string `yaml:",omitempty"` 45 Grade string `yaml:",omitempty"` 46 Confinement string `yaml:",omitempty"` 47 Architectures []string 48 Layout map[string]LayoutMetadata `yaml:",omitempty"` 49 Apps map[string]AppMetadata 50 Plugs map[string]interface{} `yaml:",omitempty"` 51 } 52 53 // AppMetadata for the binaries that will be in the snap package. 54 type AppMetadata struct { 55 Command string 56 Plugs []string `yaml:",omitempty"` 57 Daemon string `yaml:",omitempty"` 58 Completer string `yaml:",omitempty"` 59 RestartCondition string `yaml:"restart-condition,omitempty"` 60 } 61 62 type LayoutMetadata struct { 63 Symlink string `yaml:",omitempty"` 64 Bind string `yaml:",omitempty"` 65 BindFile string `yaml:"bind-file,omitempty"` 66 Type string `yaml:",omitempty"` 67 } 68 69 const defaultNameTemplate = "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" 70 71 // Pipe for snapcraft packaging. 72 type Pipe struct{} 73 74 func (Pipe) String() string { return "snapcraft packages" } 75 func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Snapcrafts) == 0 } 76 77 // Default sets the pipe defaults. 78 func (Pipe) Default(ctx *context.Context) error { 79 ids := ids.New("snapcrafts") 80 for i := range ctx.Config.Snapcrafts { 81 snap := &ctx.Config.Snapcrafts[i] 82 if snap.NameTemplate == "" { 83 snap.NameTemplate = defaultNameTemplate 84 } 85 if len(snap.ChannelTemplates) == 0 { 86 switch snap.Grade { 87 case "devel": 88 snap.ChannelTemplates = []string{"edge", "beta"} 89 default: 90 snap.ChannelTemplates = []string{"edge", "beta", "candidate", "stable"} 91 } 92 } 93 if len(snap.Builds) == 0 { 94 for _, b := range ctx.Config.Builds { 95 snap.Builds = append(snap.Builds, b.ID) 96 } 97 } 98 ids.Inc(snap.ID) 99 } 100 return ids.Validate() 101 } 102 103 // Run the pipe. 104 func (Pipe) Run(ctx *context.Context) error { 105 for _, snap := range ctx.Config.Snapcrafts { 106 // TODO: deal with pipe.skip? 107 if err := doRun(ctx, snap); err != nil { 108 return err 109 } 110 } 111 return nil 112 } 113 114 func doRun(ctx *context.Context, snap config.Snapcraft) error { 115 if snap.Summary == "" && snap.Description == "" { 116 return pipe.Skip("no summary nor description were provided") 117 } 118 if snap.Summary == "" { 119 return ErrNoSummary 120 } 121 if snap.Description == "" { 122 return ErrNoDescription 123 } 124 _, err := exec.LookPath("snapcraft") 125 if err != nil { 126 return ErrNoSnapcraft 127 } 128 129 g := semerrgroup.New(ctx.Parallelism) 130 for platform, binaries := range ctx.Artifacts.Filter( 131 artifact.And( 132 artifact.ByGoos("linux"), 133 artifact.ByType(artifact.Binary), 134 artifact.ByIDs(snap.Builds...), 135 ), 136 ).GroupByPlatform() { 137 arch := linux.Arch(platform) 138 if !isValidArch(arch) { 139 log.WithField("arch", arch).Warn("ignored unsupported arch") 140 continue 141 } 142 binaries := binaries 143 g.Go(func() error { 144 return create(ctx, snap, arch, binaries) 145 }) 146 } 147 return g.Wait() 148 } 149 150 func isValidArch(arch string) bool { 151 // https://snapcraft.io/docs/architectures 152 for _, a := range []string{"s390x", "ppc64el", "arm64", "armhf", "amd64", "i386"} { 153 if arch == a { 154 return true 155 } 156 } 157 return false 158 } 159 160 // Publish packages. 161 func (Pipe) Publish(ctx *context.Context) error { 162 if ctx.SkipPublish { 163 return pipe.ErrSkipPublishEnabled 164 } 165 snaps := ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableSnapcraft)).List() 166 g := semerrgroup.New(ctx.Parallelism) 167 for _, snap := range snaps { 168 snap := snap 169 g.Go(func() error { 170 return push(ctx, snap) 171 }) 172 } 173 return g.Wait() 174 } 175 176 func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries []*artifact.Artifact) error { 177 log := log.WithField("arch", arch) 178 folder, err := tmpl.New(ctx). 179 WithArtifact(binaries[0], snap.Replacements). 180 Apply(snap.NameTemplate) 181 if err != nil { 182 return err 183 } 184 185 channels, err := processChannelsTemplates(ctx, snap) 186 if err != nil { 187 return err 188 } 189 190 // prime is the directory that then will be compressed to make the .snap package. 191 folderDir := filepath.Join(ctx.Config.Dist, folder) 192 primeDir := filepath.Join(folderDir, "prime") 193 metaDir := filepath.Join(primeDir, "meta") 194 // #nosec 195 if err = os.MkdirAll(metaDir, 0o755); err != nil { 196 return err 197 } 198 199 for _, file := range snap.Files { 200 if file.Destination == "" { 201 file.Destination = file.Source 202 } 203 if file.Mode == 0 { 204 file.Mode = 0o644 205 } 206 destinationDir := filepath.Join(primeDir, filepath.Dir(file.Destination)) 207 if err := os.MkdirAll(destinationDir, 0o755); err != nil { 208 return fmt.Errorf("failed to create directory '%s': %w", destinationDir, err) 209 } 210 if err := gio.CopyWithMode(file.Source, filepath.Join(primeDir, file.Destination), os.FileMode(file.Mode)); err != nil { 211 return fmt.Errorf("failed to link extra file '%s': %w", file.Source, err) 212 } 213 } 214 215 file := filepath.Join(primeDir, "meta", "snap.yaml") 216 log.WithField("file", file).Debug("creating snap metadata") 217 218 metadata := &Metadata{ 219 Version: ctx.Version, 220 Summary: snap.Summary, 221 Description: snap.Description, 222 Grade: snap.Grade, 223 Confinement: snap.Confinement, 224 Architectures: []string{arch}, 225 Layout: map[string]LayoutMetadata{}, 226 Apps: map[string]AppMetadata{}, 227 } 228 229 if snap.Base != "" { 230 metadata.Base = snap.Base 231 } 232 233 if snap.License != "" { 234 metadata.License = snap.License 235 } 236 237 metadata.Name = ctx.Config.ProjectName 238 if snap.Name != "" { 239 metadata.Name = snap.Name 240 } 241 242 for targetPath, layout := range snap.Layout { 243 metadata.Layout[targetPath] = LayoutMetadata{ 244 Symlink: layout.Symlink, 245 Bind: layout.Bind, 246 BindFile: layout.BindFile, 247 Type: layout.Type, 248 } 249 } 250 251 // if the user didn't specify any apps then 252 // default to the main binary being the command: 253 if len(snap.Apps) == 0 { 254 name := snap.Name 255 if name == "" { 256 name = filepath.Base(binaries[0].Name) 257 } 258 metadata.Apps[name] = AppMetadata{ 259 Command: filepath.Base(filepath.Base(binaries[0].Name)), 260 } 261 } 262 263 for _, binary := range binaries { 264 // build the binaries and link resources 265 destBinaryPath := filepath.Join(primeDir, filepath.Base(binary.Path)) 266 log.WithField("src", binary.Path). 267 WithField("dst", destBinaryPath). 268 Debug("copying") 269 270 if err = gio.CopyWithMode(binary.Path, destBinaryPath, 0o555); err != nil { 271 return fmt.Errorf("failed to copy binary: %w", err) 272 } 273 } 274 275 // setup the apps: directive for each binary 276 for name, config := range snap.Apps { 277 command := name 278 if config.Command != "" { 279 command = config.Command 280 } 281 282 // TODO: test that the correct binary is used in Command 283 // See https://github.com/goreleaser/goreleaser/pull/1449 284 appMetadata := AppMetadata{ 285 Command: strings.TrimSpace(strings.Join([]string{ 286 command, 287 config.Args, 288 }, " ")), 289 Plugs: config.Plugs, 290 Daemon: config.Daemon, 291 RestartCondition: config.RestartCondition, 292 } 293 294 if config.Completer != "" { 295 destCompleterPath := filepath.Join(primeDir, config.Completer) 296 if err := os.MkdirAll(filepath.Dir(destCompleterPath), 0o755); err != nil { 297 return fmt.Errorf("failed to create folder: %w", err) 298 } 299 log.WithField("src", config.Completer). 300 WithField("dst", destCompleterPath). 301 Debug("copy") 302 303 if err := gio.CopyWithMode(config.Completer, destCompleterPath, 0o644); err != nil { 304 return fmt.Errorf("failed to copy completer: %w", err) 305 } 306 307 appMetadata.Completer = config.Completer 308 } 309 310 metadata.Apps[name] = appMetadata 311 metadata.Plugs = snap.Plugs 312 } 313 314 out, err := yaml.Marshal(metadata) 315 if err != nil { 316 return err 317 } 318 319 log.WithField("file", file).Debugf("writing metadata file") 320 if err = os.WriteFile(file, out, 0o644); err != nil { //nolint: gosec 321 return err 322 } 323 324 snapFile := filepath.Join(ctx.Config.Dist, folder+".snap") 325 log.WithField("snap", snapFile).Info("creating") 326 /* #nosec */ 327 cmd := exec.CommandContext(ctx, "snapcraft", "pack", primeDir, "--output", snapFile) 328 if out, err = cmd.CombinedOutput(); err != nil { 329 return fmt.Errorf("failed to generate snap package: %w: %s", err, string(out)) 330 } 331 if !snap.Publish { 332 return nil 333 } 334 ctx.Artifacts.Add(&artifact.Artifact{ 335 Type: artifact.PublishableSnapcraft, 336 Name: folder + ".snap", 337 Path: snapFile, 338 Goos: binaries[0].Goos, 339 Goarch: binaries[0].Goarch, 340 Goarm: binaries[0].Goarm, 341 Extra: map[string]interface{}{ 342 releasesExtra: channels, 343 }, 344 }) 345 return nil 346 } 347 348 const ( 349 reviewWaitMsg = `Waiting for previous upload(s) to complete their review process.` 350 humanReviewMsg = `A human will soon review your snap` 351 needsReviewMsg = `(NEEDS REVIEW)` 352 ) 353 354 func push(ctx *context.Context, snap *artifact.Artifact) error { 355 log := log.WithField("snap", snap.Name) 356 releases := snap.Extra[releasesExtra].([]string) 357 /* #nosec */ 358 cmd := exec.CommandContext(ctx, "snapcraft", "upload", "--release="+strings.Join(releases, ","), snap.Path) 359 log.WithField("args", cmd.Args).Info("pushing snap") 360 if out, err := cmd.CombinedOutput(); err != nil { 361 if strings.Contains(string(out), reviewWaitMsg) || strings.Contains(string(out), humanReviewMsg) || strings.Contains(string(out), needsReviewMsg) { 362 log.Warn(reviewWaitMsg) 363 } else { 364 return fmt.Errorf("failed to push %s package: %w: %s", snap.Path, err, string(out)) 365 } 366 } 367 snap.Type = artifact.Snapcraft 368 ctx.Artifacts.Add(snap) 369 return nil 370 } 371 372 func processChannelsTemplates(ctx *context.Context, snap config.Snapcraft) ([]string, error) { 373 // nolint:prealloc 374 var channels []string 375 for _, channeltemplate := range snap.ChannelTemplates { 376 channel, err := tmpl.New(ctx).Apply(channeltemplate) 377 if err != nil { 378 return nil, fmt.Errorf("failed to execute channel template '%s': %w", err, err) 379 } 380 if channel == "" { 381 continue 382 } 383 384 channels = append(channels, channel) 385 } 386 387 return channels, nil 388 }