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  }