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  }