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