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