github.com/triarius/goreleaser@v1.12.5/internal/pipe/nfpm/nfpm.go (about) 1 // Package nfpm implements the Pipe interface providing nFPM bindings. 2 package nfpm 3 4 import ( 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/caarlos0/log" 12 "github.com/triarius/goreleaser/internal/artifact" 13 "github.com/triarius/goreleaser/internal/deprecate" 14 "github.com/triarius/goreleaser/internal/ids" 15 "github.com/triarius/goreleaser/internal/pipe" 16 "github.com/triarius/goreleaser/internal/semerrgroup" 17 "github.com/triarius/goreleaser/internal/tmpl" 18 "github.com/triarius/goreleaser/pkg/config" 19 "github.com/triarius/goreleaser/pkg/context" 20 "github.com/goreleaser/nfpm/v2" 21 "github.com/goreleaser/nfpm/v2/deprecation" 22 "github.com/goreleaser/nfpm/v2/files" 23 "github.com/imdario/mergo" 24 25 _ "github.com/goreleaser/nfpm/v2/apk" // blank import to register the format 26 _ "github.com/goreleaser/nfpm/v2/deb" // blank import to register the format 27 _ "github.com/goreleaser/nfpm/v2/rpm" // blank import to register the format 28 ) 29 30 const ( 31 defaultNameTemplate = `{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}` 32 extraFiles = "Files" 33 ) 34 35 // Pipe for nfpm packaging. 36 type Pipe struct{} 37 38 func (Pipe) String() string { return "linux packages" } 39 func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.NFPMs) == 0 } 40 41 // Default sets the pipe defaults. 42 func (Pipe) Default(ctx *context.Context) error { 43 ids := ids.New("nfpms") 44 for i := range ctx.Config.NFPMs { 45 fpm := &ctx.Config.NFPMs[i] 46 if fpm.ID == "" { 47 fpm.ID = "default" 48 } 49 if fpm.Bindir == "" { 50 fpm.Bindir = "/usr/bin" 51 } 52 if fpm.PackageName == "" { 53 fpm.PackageName = ctx.Config.ProjectName 54 } 55 if fpm.FileNameTemplate == "" { 56 fpm.FileNameTemplate = defaultNameTemplate 57 } 58 if fpm.Maintainer == "" { 59 deprecate.NoticeCustom(ctx, "nfpms.maintainer", "`{{ .Property }}` should always be set, check {{ .URL }} for more info") 60 } 61 ids.Inc(fpm.ID) 62 } 63 64 deprecation.Noticer = io.Discard 65 return ids.Validate() 66 } 67 68 // Run the pipe. 69 func (Pipe) Run(ctx *context.Context) error { 70 for _, nfpm := range ctx.Config.NFPMs { 71 if len(nfpm.Formats) == 0 { 72 // FIXME: this assumes other nfpm configs will fail too... 73 return pipe.Skip("no output formats configured") 74 } 75 if err := doRun(ctx, nfpm); err != nil { 76 return err 77 } 78 } 79 return nil 80 } 81 82 func doRun(ctx *context.Context, fpm config.NFPM) error { 83 filters := []artifact.Filter{ 84 artifact.ByType(artifact.Binary), 85 artifact.Or( 86 artifact.ByGoos("linux"), 87 artifact.ByGoos("ios"), 88 ), 89 } 90 if len(fpm.Builds) > 0 { 91 filters = append(filters, artifact.ByIDs(fpm.Builds...)) 92 } 93 linuxBinaries := ctx.Artifacts. 94 Filter(artifact.And(filters...)). 95 GroupByPlatform() 96 if len(linuxBinaries) == 0 { 97 return fmt.Errorf("no linux binaries found for builds %v", fpm.Builds) 98 } 99 g := semerrgroup.New(ctx.Parallelism) 100 for _, format := range fpm.Formats { 101 for _, artifacts := range linuxBinaries { 102 format := format 103 artifacts := artifacts 104 g.Go(func() error { 105 return create(ctx, fpm, format, artifacts) 106 }) 107 } 108 } 109 return g.Wait() 110 } 111 112 func mergeOverrides(fpm config.NFPM, format string) (*config.NFPMOverridables, error) { 113 var overridden config.NFPMOverridables 114 if err := mergo.Merge(&overridden, fpm.NFPMOverridables); err != nil { 115 return nil, err 116 } 117 perFormat, ok := fpm.Overrides[format] 118 if ok { 119 err := mergo.Merge(&overridden, perFormat, mergo.WithOverride) 120 if err != nil { 121 return nil, err 122 } 123 } 124 return &overridden, nil 125 } 126 127 const termuxFormat = "termux.deb" 128 129 func isSupportedTermuxArch(arch string) bool { 130 for _, a := range []string{"amd64", "arm64", "386"} { 131 if strings.HasPrefix(arch, a) { 132 return true 133 } 134 } 135 return false 136 } 137 138 func create(ctx *context.Context, fpm config.NFPM, format string, binaries []*artifact.Artifact) error { 139 // TODO: improve mips handling on nfpm 140 infoArch := binaries[0].Goarch + binaries[0].Goarm + binaries[0].Gomips // key used for the ConventionalFileName et al 141 arch := infoArch + binaries[0].Goamd64 // unique arch key 142 infoPlatform := binaries[0].Goos 143 if infoPlatform == "ios" { 144 infoPlatform = "iphoneos-arm64" 145 } 146 147 bindDir := fpm.Bindir 148 if format == termuxFormat { 149 if !isSupportedTermuxArch(arch) { 150 log.Debugf("skipping termux.deb for %s as its not supported by termux", arch) 151 return nil 152 } 153 154 replacer := strings.NewReplacer( 155 "386", "i686", 156 "amd64", "x86_64", 157 "arm64", "aarch64", 158 ) 159 infoArch = replacer.Replace(infoArch) 160 arch = replacer.Replace(arch) 161 bindDir = filepath.Join("/data/data/com.termux/files", bindDir) 162 } 163 164 overridden, err := mergeOverrides(fpm, format) 165 if err != nil { 166 return err 167 } 168 t := tmpl.New(ctx). 169 WithArtifact(binaries[0], overridden.Replacements). 170 WithExtraFields(tmpl.Fields{ 171 "Release": fpm.Release, 172 "Epoch": fpm.Epoch, 173 "PackageName": fpm.PackageName, 174 }) 175 176 binDir, err := t.Apply(bindDir) 177 if err != nil { 178 return err 179 } 180 181 homepage, err := t.Apply(fpm.Homepage) 182 if err != nil { 183 return err 184 } 185 186 description, err := t.Apply(fpm.Description) 187 if err != nil { 188 return err 189 } 190 191 maintainer, err := t.Apply(fpm.Maintainer) 192 if err != nil { 193 return err 194 } 195 196 debKeyFile, err := t.Apply(overridden.Deb.Signature.KeyFile) 197 if err != nil { 198 return err 199 } 200 201 rpmKeyFile, err := t.Apply(overridden.RPM.Signature.KeyFile) 202 if err != nil { 203 return err 204 } 205 206 apkKeyFile, err := t.Apply(overridden.APK.Signature.KeyFile) 207 if err != nil { 208 return err 209 } 210 211 contents := files.Contents{} 212 for _, content := range overridden.Contents { 213 src, err := t.Apply(content.Source) 214 if err != nil { 215 return err 216 } 217 dst, err := t.Apply(content.Destination) 218 if err != nil { 219 return err 220 } 221 contents = append(contents, &files.Content{ 222 Source: src, 223 Destination: dst, 224 Type: content.Type, 225 Packager: content.Packager, 226 FileInfo: content.FileInfo, 227 }) 228 } 229 230 if len(fpm.Deb.Lintian) > 0 { 231 lines := make([]string, 0, len(fpm.Deb.Lintian)) 232 for _, ov := range fpm.Deb.Lintian { 233 lines = append(lines, fmt.Sprintf("%s: %s", fpm.PackageName, ov)) 234 } 235 lintianPath := filepath.Join(ctx.Config.Dist, "deb", fpm.PackageName+"_"+arch, ".lintian") 236 if err := os.MkdirAll(filepath.Dir(lintianPath), 0o755); err != nil { 237 return fmt.Errorf("failed to write lintian file: %w", err) 238 } 239 if err := os.WriteFile(lintianPath, []byte(strings.Join(lines, "\n")), 0o644); err != nil { 240 return fmt.Errorf("failed to write lintian file: %w", err) 241 } 242 243 log.Debugf("creating %q", lintianPath) 244 contents = append(contents, &files.Content{ 245 Source: lintianPath, 246 Destination: filepath.Join("./usr/share/lintian/overrides", fpm.PackageName), 247 Packager: "deb", 248 FileInfo: &files.ContentFileInfo{ 249 Mode: 0o644, 250 }, 251 }) 252 } 253 254 log := log.WithField("package", fpm.PackageName).WithField("format", format).WithField("arch", arch) 255 256 // FPM meta package should not contain binaries at all 257 if !fpm.Meta { 258 for _, binary := range binaries { 259 src := binary.Path 260 dst := filepath.Join(binDir, binary.Name) 261 log.WithField("src", src).WithField("dst", dst).Debug("adding binary to package") 262 contents = append(contents, &files.Content{ 263 Source: filepath.ToSlash(src), 264 Destination: filepath.ToSlash(dst), 265 FileInfo: &files.ContentFileInfo{ 266 Mode: 0o755, 267 }, 268 }) 269 } 270 } 271 272 log.WithField("files", destinations(contents)).Debug("all archive files") 273 274 info := &nfpm.Info{ 275 Arch: infoArch, 276 Platform: infoPlatform, 277 Name: fpm.PackageName, 278 Version: ctx.Version, 279 Section: fpm.Section, 280 Priority: fpm.Priority, 281 Epoch: fpm.Epoch, 282 Release: fpm.Release, 283 Prerelease: fpm.Prerelease, 284 VersionMetadata: fpm.VersionMetadata, 285 Maintainer: maintainer, 286 Description: description, 287 Vendor: fpm.Vendor, 288 Homepage: homepage, 289 License: fpm.License, 290 Changelog: fpm.Changelog, 291 Overridables: nfpm.Overridables{ 292 Conflicts: overridden.Conflicts, 293 Depends: overridden.Dependencies, 294 Recommends: overridden.Recommends, 295 Provides: overridden.Provides, 296 Suggests: overridden.Suggests, 297 Replaces: overridden.Replaces, 298 Contents: contents, 299 Scripts: nfpm.Scripts{ 300 PreInstall: overridden.Scripts.PreInstall, 301 PostInstall: overridden.Scripts.PostInstall, 302 PreRemove: overridden.Scripts.PreRemove, 303 PostRemove: overridden.Scripts.PostRemove, 304 }, 305 Deb: nfpm.Deb{ 306 // TODO: Compression, Fields 307 Scripts: nfpm.DebScripts{ 308 Rules: overridden.Deb.Scripts.Rules, 309 Templates: overridden.Deb.Scripts.Templates, 310 }, 311 Triggers: nfpm.DebTriggers{ 312 Interest: overridden.Deb.Triggers.Interest, 313 InterestAwait: overridden.Deb.Triggers.InterestAwait, 314 InterestNoAwait: overridden.Deb.Triggers.InterestNoAwait, 315 Activate: overridden.Deb.Triggers.Activate, 316 ActivateAwait: overridden.Deb.Triggers.ActivateAwait, 317 ActivateNoAwait: overridden.Deb.Triggers.ActivateNoAwait, 318 }, 319 Breaks: overridden.Deb.Breaks, 320 Signature: nfpm.DebSignature{ 321 PackageSignature: nfpm.PackageSignature{ 322 KeyFile: debKeyFile, 323 KeyPassphrase: getPassphraseFromEnv(ctx, "DEB", fpm.ID), 324 // TODO: Method, Type, KeyID 325 }, 326 Type: overridden.Deb.Signature.Type, 327 }, 328 }, 329 RPM: nfpm.RPM{ 330 Summary: overridden.RPM.Summary, 331 Group: overridden.RPM.Group, 332 Compression: overridden.RPM.Compression, 333 Signature: nfpm.RPMSignature{ 334 PackageSignature: nfpm.PackageSignature{ 335 KeyFile: rpmKeyFile, 336 KeyPassphrase: getPassphraseFromEnv(ctx, "RPM", fpm.ID), 337 // TODO: KeyID 338 }, 339 }, 340 Scripts: nfpm.RPMScripts{ 341 PreTrans: overridden.RPM.Scripts.PreTrans, 342 PostTrans: overridden.RPM.Scripts.PostTrans, 343 }, 344 }, 345 APK: nfpm.APK{ 346 Signature: nfpm.APKSignature{ 347 PackageSignature: nfpm.PackageSignature{ 348 KeyFile: apkKeyFile, 349 KeyPassphrase: getPassphraseFromEnv(ctx, "APK", fpm.ID), 350 }, 351 KeyName: overridden.APK.Signature.KeyName, 352 }, 353 Scripts: nfpm.APKScripts{ 354 PreUpgrade: overridden.APK.Scripts.PreUpgrade, 355 PostUpgrade: overridden.APK.Scripts.PostUpgrade, 356 }, 357 }, 358 }, 359 } 360 361 if ctx.SkipSign { 362 info.APK.Signature = nfpm.APKSignature{} 363 info.RPM.Signature = nfpm.RPMSignature{} 364 info.Deb.Signature = nfpm.DebSignature{} 365 } 366 367 packager, err := nfpm.Get(strings.Replace(format, "termux.", "", 1)) 368 if err != nil { 369 return err 370 } 371 372 info = nfpm.WithDefaults(info) 373 name, err := t.WithExtraFields(tmpl.Fields{ 374 "ConventionalFileName": packager.ConventionalFileName(info), 375 }).Apply(overridden.FileNameTemplate) 376 if err != nil { 377 return err 378 } 379 380 ext := "." + format 381 if packager, ok := packager.(nfpm.PackagerWithExtension); ok { 382 if format != "termux.deb" { 383 ext = packager.ConventionalExtension() 384 } 385 } 386 387 if !strings.HasSuffix(name, ext) { 388 name = name + ext 389 } 390 391 path := filepath.Join(ctx.Config.Dist, name) 392 log.WithField("file", path).Info("creating") 393 w, err := os.Create(path) 394 if err != nil { 395 return err 396 } 397 defer w.Close() 398 399 if err := packager.Package(info, w); err != nil { 400 return fmt.Errorf("nfpm failed: %w", err) 401 } 402 if err := w.Close(); err != nil { 403 return fmt.Errorf("could not close package file: %w", err) 404 } 405 ctx.Artifacts.Add(&artifact.Artifact{ 406 Type: artifact.LinuxPackage, 407 Name: name, 408 Path: path, 409 Goos: binaries[0].Goos, 410 Goarch: binaries[0].Goarch, 411 Goarm: binaries[0].Goarm, 412 Gomips: binaries[0].Gomips, 413 Goamd64: binaries[0].Goamd64, 414 Extra: map[string]interface{}{ 415 artifact.ExtraBuilds: binaries, 416 artifact.ExtraID: fpm.ID, 417 artifact.ExtraFormat: format, 418 extraFiles: contents, 419 }, 420 }) 421 return nil 422 } 423 424 func destinations(contents files.Contents) []string { 425 result := make([]string, 0, len(contents)) 426 for _, f := range contents { 427 result = append(result, f.Destination) 428 } 429 return result 430 } 431 432 func getPassphraseFromEnv(ctx *context.Context, packager string, nfpmID string) string { 433 var passphrase string 434 435 nfpmID = strings.ToUpper(nfpmID) 436 packagerSpecificPassphrase := ctx.Env[fmt.Sprintf( 437 "NFPM_%s_%s_PASSPHRASE", 438 nfpmID, 439 packager, 440 )] 441 if packagerSpecificPassphrase != "" { 442 passphrase = packagerSpecificPassphrase 443 } else { 444 generalPassphrase := ctx.Env[fmt.Sprintf("NFPM_%s_PASSPHRASE", nfpmID)] 445 passphrase = generalPassphrase 446 } 447 448 return passphrase 449 }