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  }