github.com/goreleaser/goreleaser@v1.25.1/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 "dario.cat/mergo" 12 "github.com/caarlos0/log" 13 "github.com/goreleaser/goreleaser/internal/artifact" 14 "github.com/goreleaser/goreleaser/internal/deprecate" 15 "github.com/goreleaser/goreleaser/internal/ids" 16 "github.com/goreleaser/goreleaser/internal/pipe" 17 "github.com/goreleaser/goreleaser/internal/semerrgroup" 18 "github.com/goreleaser/goreleaser/internal/skips" 19 "github.com/goreleaser/goreleaser/internal/tmpl" 20 "github.com/goreleaser/goreleaser/pkg/config" 21 "github.com/goreleaser/goreleaser/pkg/context" 22 "github.com/goreleaser/nfpm/v2" 23 "github.com/goreleaser/nfpm/v2/deprecation" 24 "github.com/goreleaser/nfpm/v2/files" 25 26 _ "github.com/goreleaser/nfpm/v2/apk" // blank import to register the format 27 _ "github.com/goreleaser/nfpm/v2/arch" // blank import to register the format 28 _ "github.com/goreleaser/nfpm/v2/deb" // blank import to register the format 29 _ "github.com/goreleaser/nfpm/v2/rpm" // blank import to register the format 30 ) 31 32 const ( 33 defaultNameTemplate = `{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}` 34 extraFiles = "Files" 35 ) 36 37 // Pipe for nfpm packaging. 38 type Pipe struct{} 39 40 func (Pipe) String() string { return "linux packages" } 41 func (Pipe) Skip(ctx *context.Context) bool { 42 return skips.Any(ctx, skips.NFPM) || len(ctx.Config.NFPMs) == 0 43 } 44 45 // Default sets the pipe defaults. 46 func (Pipe) Default(ctx *context.Context) error { 47 ids := ids.New("nfpms") 48 for i := range ctx.Config.NFPMs { 49 fpm := &ctx.Config.NFPMs[i] 50 if fpm.ID == "" { 51 fpm.ID = "default" 52 } 53 if fpm.Bindir == "" { 54 fpm.Bindir = "/usr/bin" 55 } 56 if fpm.Libdirs.Header == "" { 57 fpm.Libdirs.Header = "/usr/include" 58 } 59 if fpm.Libdirs.CShared == "" { 60 fpm.Libdirs.CShared = "/usr/lib" 61 } 62 if fpm.Libdirs.CArchive == "" { 63 fpm.Libdirs.CArchive = "/usr/lib" 64 } 65 if fpm.PackageName == "" { 66 fpm.PackageName = ctx.Config.ProjectName 67 } 68 if fpm.FileNameTemplate == "" { 69 fpm.FileNameTemplate = defaultNameTemplate 70 } 71 if fpm.Maintainer == "" { 72 deprecate.NoticeCustom(ctx, "nfpms.maintainer", "`{{ .Property }}` should always be set, check {{ .URL }} for more info") 73 } 74 ids.Inc(fpm.ID) 75 } 76 77 deprecation.Noticer = io.Discard 78 return ids.Validate() 79 } 80 81 // Run the pipe. 82 func (Pipe) Run(ctx *context.Context) error { 83 for _, nfpm := range ctx.Config.NFPMs { 84 if len(nfpm.Formats) == 0 { 85 // FIXME: this assumes other nfpm configs will fail too... 86 return pipe.Skip("no output formats configured") 87 } 88 if err := doRun(ctx, nfpm); err != nil { 89 return err 90 } 91 } 92 return nil 93 } 94 95 func doRun(ctx *context.Context, fpm config.NFPM) error { 96 filters := []artifact.Filter{ 97 artifact.Or( 98 artifact.ByType(artifact.Binary), 99 artifact.ByType(artifact.Header), 100 artifact.ByType(artifact.CArchive), 101 artifact.ByType(artifact.CShared), 102 ), 103 artifact.Or( 104 artifact.ByGoos("linux"), 105 artifact.ByGoos("ios"), 106 ), 107 } 108 if len(fpm.Builds) > 0 { 109 filters = append(filters, artifact.ByIDs(fpm.Builds...)) 110 } 111 linuxBinaries := ctx.Artifacts. 112 Filter(artifact.And(filters...)). 113 GroupByPlatform() 114 if len(linuxBinaries) == 0 { 115 return fmt.Errorf("no linux binaries found for builds %v", fpm.Builds) 116 } 117 g := semerrgroup.New(ctx.Parallelism) 118 for _, format := range fpm.Formats { 119 for _, artifacts := range linuxBinaries { 120 format := format 121 artifacts := artifacts 122 g.Go(func() error { 123 return create(ctx, fpm, format, artifacts) 124 }) 125 } 126 } 127 return g.Wait() 128 } 129 130 func mergeOverrides(fpm config.NFPM, format string) (*config.NFPMOverridables, error) { 131 var overridden config.NFPMOverridables 132 if err := mergo.Merge(&overridden, fpm.NFPMOverridables); err != nil { 133 return nil, err 134 } 135 perFormat, ok := fpm.Overrides[format] 136 if ok { 137 err := mergo.Merge(&overridden, perFormat, mergo.WithOverride) 138 if err != nil { 139 return nil, err 140 } 141 } 142 return &overridden, nil 143 } 144 145 const termuxFormat = "termux.deb" 146 147 func isSupportedTermuxArch(arch string) bool { 148 for _, a := range []string{"amd64", "arm64", "386"} { 149 if strings.HasPrefix(arch, a) { 150 return true 151 } 152 } 153 return false 154 } 155 156 // arch officially only supports x86_64. 157 // however, there are unofficial ports for 686, arm64, and armv7 158 func isSupportedArchlinuxArch(arch, arm string) bool { 159 if arch == "arm" && arm == "7" { 160 return true 161 } 162 for _, a := range []string{"amd64", "arm64", "386"} { 163 if strings.HasPrefix(arch, a) { 164 return true 165 } 166 } 167 return false 168 } 169 170 func create(ctx *context.Context, fpm config.NFPM, format string, artifacts []*artifact.Artifact) error { 171 // TODO: improve mips handling on nfpm 172 infoArch := artifacts[0].Goarch + artifacts[0].Goarm + artifacts[0].Gomips // key used for the ConventionalFileName et al 173 arch := infoArch + artifacts[0].Goamd64 // unique arch key 174 infoPlatform := artifacts[0].Goos 175 if infoPlatform == "ios" { 176 if format == "deb" { 177 infoPlatform = "iphoneos-arm64" 178 } else { 179 log.Debugf("skipping ios for %s as its not supported", format) 180 return nil 181 } 182 } 183 184 switch format { 185 case "archlinux": 186 if !isSupportedArchlinuxArch(artifacts[0].Goarch, artifacts[0].Goarm) { 187 log.Debugf("skipping archlinux for %s as its not supported", arch) 188 return nil 189 } 190 case termuxFormat: 191 if !isSupportedTermuxArch(artifacts[0].Goarch) { 192 log.Debugf("skipping termux.deb for %s as its not supported by termux", arch) 193 return nil 194 } 195 196 replacer := strings.NewReplacer( 197 "386", "i686", 198 "amd64", "x86_64", 199 "arm64", "aarch64", 200 ) 201 infoArch = replacer.Replace(infoArch) 202 arch = replacer.Replace(arch) 203 fpm.Bindir = termuxPrefixedDir(fpm.Bindir) 204 fpm.Libdirs.Header = termuxPrefixedDir(fpm.Libdirs.Header) 205 fpm.Libdirs.CArchive = termuxPrefixedDir(fpm.Libdirs.CArchive) 206 fpm.Libdirs.CShared = termuxPrefixedDir(fpm.Libdirs.CShared) 207 } 208 209 overridden, err := mergeOverrides(fpm, format) 210 if err != nil { 211 return err 212 } 213 214 packageName, err := tmpl.New(ctx).Apply(fpm.PackageName) 215 if err != nil { 216 return err 217 } 218 219 t := tmpl.New(ctx). 220 WithArtifact(artifacts[0]). 221 WithExtraFields(tmpl.Fields{ 222 "Release": fpm.Release, 223 "Epoch": fpm.Epoch, 224 "PackageName": packageName, 225 }) 226 227 if err := t.ApplyAll( 228 &fpm.Bindir, 229 &fpm.Homepage, 230 &fpm.Description, 231 &fpm.Maintainer, 232 &overridden.Scripts.PostInstall, 233 &overridden.Scripts.PreInstall, 234 &overridden.Scripts.PostRemove, 235 &overridden.Scripts.PreRemove, 236 ); err != nil { 237 return err 238 } 239 240 // We cannot use t.ApplyAll on the following fields as they are shared 241 // across multiple nfpms. 242 // 243 t = t.WithExtraFields(tmpl.Fields{ 244 "Format": format, 245 }) 246 247 debKeyFile, err := t.Apply(overridden.Deb.Signature.KeyFile) 248 if err != nil { 249 return err 250 } 251 252 rpmKeyFile, err := t.Apply(overridden.RPM.Signature.KeyFile) 253 if err != nil { 254 return err 255 } 256 257 apkKeyFile, err := t.Apply(overridden.APK.Signature.KeyFile) 258 if err != nil { 259 return err 260 } 261 262 apkKeyName, err := t.Apply(overridden.APK.Signature.KeyName) 263 if err != nil { 264 return err 265 } 266 267 libdirs := config.Libdirs{} 268 269 libdirs.Header, err = t.Apply(fpm.Libdirs.Header) 270 if err != nil { 271 return err 272 } 273 libdirs.CShared, err = t.Apply(fpm.Libdirs.CShared) 274 if err != nil { 275 return err 276 } 277 libdirs.CArchive, err = t.Apply(fpm.Libdirs.CArchive) 278 if err != nil { 279 return err 280 } 281 282 contents := files.Contents{} 283 for _, content := range overridden.Contents { 284 src, err := t.Apply(content.Source) 285 if err != nil { 286 return err 287 } 288 dst, err := t.Apply(content.Destination) 289 if err != nil { 290 return err 291 } 292 contents = append(contents, &files.Content{ 293 Source: src, 294 Destination: dst, 295 Type: content.Type, 296 Packager: content.Packager, 297 FileInfo: content.FileInfo, 298 }) 299 } 300 301 if len(fpm.Deb.Lintian) > 0 && (format == "deb" || format == "termux.deb") { 302 lintian, err := setupLintian(ctx, fpm, packageName, format, arch) 303 if err != nil { 304 return err 305 } 306 contents = append(contents, lintian) 307 } 308 309 log := log.WithField("package", packageName).WithField("format", format).WithField("arch", arch) 310 311 // FPM meta package should not contain binaries at all 312 if !fpm.Meta { 313 for _, art := range artifacts { 314 src := art.Path 315 dst := filepath.Join(artifactPackageDir(fpm.Bindir, libdirs, art), art.Name) 316 log.WithField("src", src). 317 WithField("dst", dst). 318 WithField("type", art.Type.String()). 319 Debug("adding artifact to package") 320 contents = append(contents, &files.Content{ 321 Source: filepath.ToSlash(src), 322 Destination: filepath.ToSlash(dst), 323 FileInfo: &files.ContentFileInfo{ 324 Mode: 0o755, 325 }, 326 }) 327 } 328 } 329 330 log.WithField("files", destinations(contents)).Debug("all archive files") 331 332 info := &nfpm.Info{ 333 Arch: infoArch, 334 Platform: infoPlatform, 335 Name: packageName, 336 Version: ctx.Version, 337 Section: fpm.Section, 338 Priority: fpm.Priority, 339 Epoch: fpm.Epoch, 340 Release: fpm.Release, 341 Prerelease: fpm.Prerelease, 342 VersionMetadata: fpm.VersionMetadata, 343 Maintainer: fpm.Maintainer, 344 Description: fpm.Description, 345 Vendor: fpm.Vendor, 346 Homepage: fpm.Homepage, 347 License: fpm.License, 348 Changelog: fpm.Changelog, 349 Overridables: nfpm.Overridables{ 350 Umask: overridden.Umask, 351 Conflicts: overridden.Conflicts, 352 Depends: overridden.Dependencies, 353 Recommends: overridden.Recommends, 354 Provides: overridden.Provides, 355 Suggests: overridden.Suggests, 356 Replaces: overridden.Replaces, 357 Contents: contents, 358 Scripts: nfpm.Scripts{ 359 PreInstall: overridden.Scripts.PreInstall, 360 PostInstall: overridden.Scripts.PostInstall, 361 PreRemove: overridden.Scripts.PreRemove, 362 PostRemove: overridden.Scripts.PostRemove, 363 }, 364 Deb: nfpm.Deb{ 365 Compression: overridden.Deb.Compression, 366 Fields: overridden.Deb.Fields, 367 Predepends: overridden.Deb.Predepends, 368 Scripts: nfpm.DebScripts{ 369 Rules: overridden.Deb.Scripts.Rules, 370 Templates: overridden.Deb.Scripts.Templates, 371 }, 372 Triggers: nfpm.DebTriggers{ 373 Interest: overridden.Deb.Triggers.Interest, 374 InterestAwait: overridden.Deb.Triggers.InterestAwait, 375 InterestNoAwait: overridden.Deb.Triggers.InterestNoAwait, 376 Activate: overridden.Deb.Triggers.Activate, 377 ActivateAwait: overridden.Deb.Triggers.ActivateAwait, 378 ActivateNoAwait: overridden.Deb.Triggers.ActivateNoAwait, 379 }, 380 Breaks: overridden.Deb.Breaks, 381 Signature: nfpm.DebSignature{ 382 PackageSignature: nfpm.PackageSignature{ 383 KeyFile: debKeyFile, 384 KeyPassphrase: getPassphraseFromEnv(ctx, "DEB", fpm.ID), 385 // TODO: Method, Type, KeyID 386 }, 387 Type: overridden.Deb.Signature.Type, 388 }, 389 }, 390 RPM: nfpm.RPM{ 391 Summary: overridden.RPM.Summary, 392 Group: overridden.RPM.Group, 393 Compression: overridden.RPM.Compression, 394 Prefixes: overridden.RPM.Prefixes, 395 Packager: overridden.RPM.Packager, 396 Signature: nfpm.RPMSignature{ 397 PackageSignature: nfpm.PackageSignature{ 398 KeyFile: rpmKeyFile, 399 KeyPassphrase: getPassphraseFromEnv(ctx, "RPM", fpm.ID), 400 // TODO: KeyID 401 }, 402 }, 403 Scripts: nfpm.RPMScripts{ 404 PreTrans: overridden.RPM.Scripts.PreTrans, 405 PostTrans: overridden.RPM.Scripts.PostTrans, 406 }, 407 }, 408 APK: nfpm.APK{ 409 Signature: nfpm.APKSignature{ 410 PackageSignature: nfpm.PackageSignature{ 411 KeyFile: apkKeyFile, 412 KeyPassphrase: getPassphraseFromEnv(ctx, "APK", fpm.ID), 413 }, 414 KeyName: apkKeyName, 415 }, 416 Scripts: nfpm.APKScripts{ 417 PreUpgrade: overridden.APK.Scripts.PreUpgrade, 418 PostUpgrade: overridden.APK.Scripts.PostUpgrade, 419 }, 420 }, 421 ArchLinux: nfpm.ArchLinux{ 422 Pkgbase: overridden.ArchLinux.Pkgbase, 423 Packager: overridden.ArchLinux.Packager, 424 Scripts: nfpm.ArchLinuxScripts{ 425 PreUpgrade: overridden.ArchLinux.Scripts.PreUpgrade, 426 PostUpgrade: overridden.ArchLinux.Scripts.PostUpgrade, 427 }, 428 }, 429 }, 430 } 431 432 if skips.Any(ctx, skips.Sign) { 433 info.APK.Signature = nfpm.APKSignature{} 434 info.RPM.Signature = nfpm.RPMSignature{} 435 info.Deb.Signature = nfpm.DebSignature{} 436 } 437 438 packager, err := nfpm.Get(strings.Replace(format, "termux.", "", 1)) 439 if err != nil { 440 return err 441 } 442 443 ext := "." + format 444 if packager, ok := packager.(nfpm.PackagerWithExtension); ok { 445 if format != "termux.deb" { 446 ext = packager.ConventionalExtension() 447 } 448 } 449 450 info = nfpm.WithDefaults(info) 451 packageFilename, err := t.WithExtraFields(tmpl.Fields{ 452 "ConventionalFileName": packager.ConventionalFileName(info), 453 "ConventionalExtension": ext, 454 }).Apply(overridden.FileNameTemplate) 455 if err != nil { 456 return err 457 } 458 459 if !strings.HasSuffix(packageFilename, ext) { 460 packageFilename = packageFilename + ext 461 } 462 463 path := filepath.Join(ctx.Config.Dist, packageFilename) 464 log.WithField("file", path).Info("creating") 465 w, err := os.Create(path) 466 if err != nil { 467 return err 468 } 469 defer w.Close() 470 471 if err := packager.Package(info, w); err != nil { 472 return fmt.Errorf("nfpm failed for %s: %w", packageFilename, err) 473 } 474 if err := w.Close(); err != nil { 475 return fmt.Errorf("could not close package file: %w", err) 476 } 477 ctx.Artifacts.Add(&artifact.Artifact{ 478 Type: artifact.LinuxPackage, 479 Name: packageFilename, 480 Path: path, 481 Goos: artifacts[0].Goos, 482 Goarch: artifacts[0].Goarch, 483 Goarm: artifacts[0].Goarm, 484 Gomips: artifacts[0].Gomips, 485 Goamd64: artifacts[0].Goamd64, 486 Extra: map[string]interface{}{ 487 artifact.ExtraID: fpm.ID, 488 artifact.ExtraFormat: format, 489 artifact.ExtraExt: format, 490 extraFiles: contents, 491 }, 492 }) 493 return nil 494 } 495 496 func setupLintian(ctx *context.Context, fpm config.NFPM, packageName, format, arch string) (*files.Content, error) { 497 lines := make([]string, 0, len(fpm.Deb.Lintian)) 498 for _, ov := range fpm.Deb.Lintian { 499 lines = append(lines, fmt.Sprintf("%s: %s", packageName, ov)) 500 } 501 lintianPath := filepath.Join(ctx.Config.Dist, format, packageName+"_"+arch, "lintian") 502 if err := os.MkdirAll(filepath.Dir(lintianPath), 0o755); err != nil { 503 return nil, fmt.Errorf("failed to write lintian file: %w", err) 504 } 505 if err := os.WriteFile(lintianPath, []byte(strings.Join(lines, "\n")), 0o644); err != nil { 506 return nil, fmt.Errorf("failed to write lintian file: %w", err) 507 } 508 509 log.Debugf("creating %q", lintianPath) 510 return &files.Content{ 511 Source: lintianPath, 512 Destination: "./usr/share/lintian/overrides/" + packageName, 513 Packager: "deb", 514 FileInfo: &files.ContentFileInfo{ 515 Mode: 0o644, 516 }, 517 }, nil 518 } 519 520 func destinations(contents files.Contents) []string { 521 result := make([]string, 0, len(contents)) 522 for _, f := range contents { 523 result = append(result, f.Destination) 524 } 525 return result 526 } 527 528 func getPassphraseFromEnv(ctx *context.Context, packager string, nfpmID string) string { 529 nfpmID = strings.ToUpper(nfpmID) 530 for _, k := range []string{ 531 fmt.Sprintf("NFPM_%s_%s_PASSPHRASE", nfpmID, packager), 532 fmt.Sprintf("NFPM_%s_PASSPHRASE", nfpmID), 533 "NFPM_PASSPHRASE", 534 } { 535 if v, ok := ctx.Env[k]; ok { 536 return v 537 } 538 } 539 540 return "" 541 } 542 543 func termuxPrefixedDir(dir string) string { 544 if dir == "" { 545 return "" 546 } 547 return filepath.Join("/data/data/com.termux/files", dir) 548 } 549 550 func artifactPackageDir(bindir string, libdirs config.Libdirs, art *artifact.Artifact) string { 551 switch art.Type { 552 case artifact.Binary: 553 return bindir 554 case artifact.Header: 555 return libdirs.Header 556 case artifact.CShared: 557 return libdirs.CShared 558 case artifact.CArchive: 559 return libdirs.CArchive 560 default: 561 // should never happen 562 return "" 563 } 564 }