github.com/goreleaser/nfpm/v2@v2.44.0/deb/deb.go (about)

     1  // Package deb implements nfpm.Packager providing .deb bindings.
     2  package deb
     3  
     4  import (
     5  	"archive/tar"
     6  	"bufio"
     7  	"bytes"
     8  	"compress/gzip"
     9  	"crypto/md5" // nolint:gas
    10  	"crypto/sha1"
    11  	"encoding/hex"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"io/fs"
    16  	"os"
    17  	"path/filepath"
    18  	"strconv"
    19  	"strings"
    20  	"text/template"
    21  	"time"
    22  
    23  	"github.com/blakesmith/ar"
    24  	"github.com/goreleaser/chglog"
    25  	"github.com/goreleaser/nfpm/v2"
    26  	"github.com/goreleaser/nfpm/v2/deprecation"
    27  	"github.com/goreleaser/nfpm/v2/files"
    28  	"github.com/goreleaser/nfpm/v2/internal/maps"
    29  	"github.com/goreleaser/nfpm/v2/internal/modtime"
    30  	"github.com/goreleaser/nfpm/v2/internal/sign"
    31  	"github.com/klauspost/compress/zstd"
    32  	"github.com/ulikunitz/xz"
    33  )
    34  
    35  const packagerName = "deb"
    36  
    37  // nolint: gochecknoinits
    38  func init() {
    39  	nfpm.RegisterPackager(packagerName, Default)
    40  }
    41  
    42  // nolint: gochecknoglobals
    43  var archToDebian = map[string]string{
    44  	"386":      "i386",
    45  	"arm64":    "arm64",
    46  	"arm5":     "armel",
    47  	"arm6":     "armhf",
    48  	"arm7":     "armhf",
    49  	"mips64le": "mips64el",
    50  	"mipsle":   "mipsel",
    51  	"ppc64le":  "ppc64el",
    52  	"s390":     "s390x",
    53  }
    54  
    55  func ensureValidArch(info *nfpm.Info) *nfpm.Info {
    56  	if info.Deb.Arch != "" {
    57  		info.Arch = info.Deb.Arch
    58  	} else if arch, ok := archToDebian[info.Arch]; ok {
    59  		info.Arch = arch
    60  	}
    61  
    62  	return info
    63  }
    64  
    65  // Default deb packager.
    66  // nolint: gochecknoglobals
    67  var Default = &Deb{}
    68  
    69  // Deb is a deb packager implementation.
    70  type Deb struct{}
    71  
    72  // ConventionalFileName returns a file name according
    73  // to the conventions for debian packages. See:
    74  // https://manpages.debian.org/buster/dpkg-dev/dpkg-name.1.en.html
    75  func (*Deb) ConventionalFileName(info *nfpm.Info) string {
    76  	info = ensureValidArch(info)
    77  
    78  	version := info.Version
    79  	if info.Prerelease != "" {
    80  		version += "~" + info.Prerelease
    81  	}
    82  
    83  	if info.VersionMetadata != "" {
    84  		version += "+" + info.VersionMetadata
    85  	}
    86  
    87  	if info.Release != "" {
    88  		version += "-" + info.Release
    89  	}
    90  
    91  	// package_version_architecture.package-type
    92  	return fmt.Sprintf("%s_%s_%s.deb", info.Name, version, info.Arch)
    93  }
    94  
    95  // ConventionalExtension returns the file name conventionally used for Deb packages
    96  func (*Deb) ConventionalExtension() string {
    97  	return ".deb"
    98  }
    99  
   100  // ErrInvalidSignatureType happens if the signature type of a deb is not one of
   101  // origin, maint or archive.
   102  var ErrInvalidSignatureType = errors.New("invalid signature type")
   103  
   104  // Package writes a new deb package to the given writer using the given info.
   105  func (d *Deb) Package(info *nfpm.Info, deb io.Writer) (err error) { // nolint: funlen
   106  	info = ensureValidArch(info)
   107  
   108  	err = nfpm.PrepareForPackager(withChangelogIfRequested(info), packagerName)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// Set up some deb specific defaults
   114  	d.SetPackagerDefaults(info)
   115  
   116  	dataTarball, md5sums, instSize, dataTarballName, err := createDataTarball(info)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	controlTarGz, err := createControl(instSize, md5sums, info)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	debianBinary := []byte("2.0\n")
   127  
   128  	w := ar.NewWriter(deb)
   129  	if err := w.WriteGlobalHeader(); err != nil {
   130  		return fmt.Errorf("cannot write ar header to deb file: %w", err)
   131  	}
   132  
   133  	mtime := modtime.Get(info.MTime)
   134  
   135  	if err := addArFile(w, "debian-binary", debianBinary, mtime); err != nil {
   136  		return fmt.Errorf("cannot pack debian-binary: %w", err)
   137  	}
   138  
   139  	if err := addArFile(w, "control.tar.gz", controlTarGz, mtime); err != nil {
   140  		return fmt.Errorf("cannot add control.tar.gz to deb: %w", err)
   141  	}
   142  
   143  	if err := addArFile(w, dataTarballName, dataTarball, mtime); err != nil {
   144  		return fmt.Errorf("cannot add data.tar.gz to deb: %w", err)
   145  	}
   146  
   147  	if info.Deb.Signature.KeyFile != "" || info.Deb.Signature.SignFn != nil {
   148  		sig, sigType, err := doSign(info, debianBinary, controlTarGz, dataTarball)
   149  		if err != nil {
   150  			return err
   151  		}
   152  
   153  		if err := addArFile(w, "_gpg"+sigType, sig, mtime); err != nil {
   154  			return &nfpm.ErrSigningFailure{
   155  				Err: fmt.Errorf("add signature to ar file: %w", err),
   156  			}
   157  		}
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func doSign(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) ([]byte, string, error) {
   164  	switch info.Deb.Signature.Method {
   165  	case "dpkg-sig":
   166  		return dpkgSign(info, debianBinary, controlTarGz, dataTarball)
   167  	default:
   168  		return debSign(info, debianBinary, controlTarGz, dataTarball)
   169  	}
   170  }
   171  
   172  func dpkgSign(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) ([]byte, string, error) {
   173  	sigType := "builder"
   174  	if info.Deb.Signature.Type != "" {
   175  		sigType = info.Deb.Signature.Type
   176  	}
   177  
   178  	data, err := readDpkgSigData(info, debianBinary, controlTarGz, dataTarball)
   179  	if err != nil {
   180  		return nil, sigType, &nfpm.ErrSigningFailure{Err: err}
   181  	}
   182  
   183  	var sig []byte
   184  	if signFn := info.Deb.Signature.SignFn; signFn != nil {
   185  		sig, err = signFn(data)
   186  	} else {
   187  		sig, err = sign.PGPClearSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
   188  	}
   189  	if err != nil {
   190  		return nil, sigType, &nfpm.ErrSigningFailure{Err: err}
   191  	}
   192  	return sig, sigType, nil
   193  }
   194  
   195  func debSign(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) ([]byte, string, error) {
   196  	data := readDebsignData(debianBinary, controlTarGz, dataTarball)
   197  
   198  	sigType := "origin"
   199  	if info.Deb.Signature.Type != "" {
   200  		sigType = info.Deb.Signature.Type
   201  	}
   202  
   203  	if sigType != "origin" && sigType != "maint" && sigType != "archive" {
   204  		return nil, sigType, &nfpm.ErrSigningFailure{
   205  			Err: ErrInvalidSignatureType,
   206  		}
   207  	}
   208  
   209  	var sig []byte
   210  	var err error
   211  	if signFn := info.Deb.Signature.SignFn; signFn != nil {
   212  		sig, err = signFn(data)
   213  	} else {
   214  		sig, err = sign.PGPArmoredDetachSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
   215  	}
   216  	if err != nil {
   217  		return nil, sigType, &nfpm.ErrSigningFailure{Err: err}
   218  	}
   219  	return sig, sigType, nil
   220  }
   221  
   222  func readDebsignData(debianBinary, controlTarGz, dataTarball []byte) io.Reader {
   223  	return io.MultiReader(bytes.NewReader(debianBinary), bytes.NewReader(controlTarGz),
   224  		bytes.NewReader(dataTarball))
   225  }
   226  
   227  // reference: https://manpages.debian.org/jessie/dpkg-sig/dpkg-sig.1.en.html
   228  const dpkgSigTemplate = `
   229  Hash: SHA1
   230  
   231  Version: 4
   232  Signer: {{ .Signer }}
   233  Date: {{ .Date }}
   234  Role: {{ .Role }}
   235  Files:
   236  {{range .Files -}}
   237  {{"\t"}}{{ hex .Md5Sum }} {{ hex .Sha1Sum }} {{ .Size }} {{ .Name }}
   238  {{end -}}
   239  `
   240  
   241  type dpkgSigData struct {
   242  	Signer string
   243  	Date   time.Time
   244  	Role   string
   245  	Files  []dpkgSigFileLine
   246  	Info   *nfpm.Info
   247  }
   248  type dpkgSigFileLine struct {
   249  	Md5Sum  []byte
   250  	Sha1Sum []byte
   251  	Size    int
   252  	Name    string
   253  }
   254  
   255  func newDpkgSigFileLine(name string, fileContent []byte) dpkgSigFileLine {
   256  	md5Sum, sha1Sum := md5.Sum(fileContent), sha1.Sum(fileContent)
   257  	return dpkgSigFileLine{
   258  		Name:    name,
   259  		Md5Sum:  md5Sum[:],
   260  		Sha1Sum: sha1Sum[:],
   261  		Size:    len(fileContent),
   262  	}
   263  }
   264  
   265  func readDpkgSigData(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) (io.Reader, error) {
   266  	data := dpkgSigData{
   267  		Signer: info.Deb.Signature.Signer,
   268  		Date:   modtime.Get(info.MTime),
   269  		Role:   info.Deb.Signature.Type,
   270  		Files: []dpkgSigFileLine{
   271  			newDpkgSigFileLine("debian-binary", debianBinary),
   272  			newDpkgSigFileLine("control.tar.gz", controlTarGz),
   273  			newDpkgSigFileLine("data.tar.gz", dataTarball),
   274  		},
   275  	}
   276  	temp, _ := template.New("dpkg-sig").Funcs(template.FuncMap{
   277  		"hex": hex.EncodeToString,
   278  	}).Parse(dpkgSigTemplate)
   279  	buf := &bytes.Buffer{}
   280  	err := temp.Execute(buf, data)
   281  	if err != nil {
   282  		return nil, fmt.Errorf("dpkg-sig template error: %w", err)
   283  	}
   284  	return buf, nil
   285  }
   286  
   287  func (*Deb) SetPackagerDefaults(info *nfpm.Info) {
   288  	// Priority should be set on all packages per:
   289  	//   https://www.debian.org/doc/debian-policy/ch-archive.html#priorities
   290  	// "optional" seems to be the safe/sane default here
   291  	if info.Priority == "" {
   292  		info.Priority = "optional"
   293  	}
   294  
   295  	// The safe thing here feels like defaulting to something like below.
   296  	// That will prevent existing configs from breaking anyway...  Wondering
   297  	// if in the long run we should be more strict about this and error when
   298  	// not set?
   299  	if info.Maintainer == "" {
   300  		deprecation.Println("Leaving the 'maintainer' field unset will not be allowed in a future version")
   301  		info.Maintainer = "Unset Maintainer <unset@localhost>"
   302  	}
   303  }
   304  
   305  func addArFile(w *ar.Writer, name string, body []byte, date time.Time) error {
   306  	header := ar.Header{
   307  		Name:    files.ToNixPath(name),
   308  		Size:    int64(len(body)),
   309  		Mode:    0o644,
   310  		ModTime: date,
   311  	}
   312  	if err := w.WriteHeader(&header); err != nil {
   313  		return fmt.Errorf("cannot write file header: %w", err)
   314  	}
   315  	_, err := w.Write(body)
   316  	return err
   317  }
   318  
   319  type nopCloser struct {
   320  	io.Writer
   321  }
   322  
   323  func (nopCloser) Close() error { return nil }
   324  
   325  func createDataTarball(info *nfpm.Info) (dataTarBall, md5sums []byte,
   326  	instSize int64, name string, err error,
   327  ) {
   328  	var (
   329  		dataTarball            bytes.Buffer
   330  		dataTarballWriteCloser io.WriteCloser
   331  	)
   332  
   333  	if info.Deb.Compression == "" {
   334  		info.Deb.Compression = "gzip:-1" // the default for now
   335  	}
   336  
   337  	parts := strings.Split(info.Deb.Compression, ":")
   338  	if len(parts) > 2 {
   339  		return nil, nil, 0, "", fmt.Errorf("malformed compressor setting: %s", info.Deb.Compression)
   340  	}
   341  
   342  	compressorType := parts[0]
   343  	compressorLevel := ""
   344  	if len(parts) == 2 {
   345  		compressorLevel = parts[1]
   346  	}
   347  
   348  	switch compressorType {
   349  	case "gzip":
   350  		level := 9
   351  		if compressorLevel != "" {
   352  			var err error
   353  			level, err = strconv.Atoi(compressorLevel)
   354  			if err != nil {
   355  				return nil, nil, 0, "", fmt.Errorf("parse gzip compressor level: %w", err)
   356  			}
   357  		}
   358  		dataTarballWriteCloser, err = gzip.NewWriterLevel(&dataTarball, level)
   359  		if err != nil {
   360  			return nil, nil, 0, "", err
   361  		}
   362  		name = "data.tar.gz"
   363  	case "xz":
   364  		if compressorLevel != "" {
   365  			return nil, nil, 0, "", fmt.Errorf("no compressor level supported for xz: %s", compressorLevel)
   366  		}
   367  		dataTarballWriteCloser, err = xz.NewWriter(&dataTarball)
   368  		if err != nil {
   369  			return nil, nil, 0, "", err
   370  		}
   371  		name = "data.tar.xz"
   372  	case "zstd":
   373  		level := zstd.SpeedBetterCompression
   374  		if compressorLevel != "" {
   375  			if intLevel, err := strconv.Atoi(compressorLevel); err == nil {
   376  				level = zstd.EncoderLevelFromZstd(intLevel)
   377  			} else {
   378  				var ok bool
   379  				ok, level = zstd.EncoderLevelFromString(compressorLevel)
   380  				if !ok {
   381  					return nil, nil, 0, "", fmt.Errorf("invalid zstd compressor level: %s", compressorLevel)
   382  				}
   383  			}
   384  		}
   385  		dataTarballWriteCloser, err = zstd.NewWriter(&dataTarball, zstd.WithEncoderLevel(level))
   386  		if err != nil {
   387  			return nil, nil, 0, "", err
   388  		}
   389  		name = "data.tar.zst"
   390  	case "none":
   391  		dataTarballWriteCloser = nopCloser{Writer: &dataTarball}
   392  		name = "data.tar"
   393  	default:
   394  		return nil, nil, 0, "", fmt.Errorf("unknown compression algorithm: %s", info.Deb.Compression)
   395  	}
   396  
   397  	// the writer is properly closed later, this is just in case that we error out
   398  	defer dataTarballWriteCloser.Close() // nolint: errcheck
   399  
   400  	md5sums, instSize, err = fillDataTar(info, dataTarballWriteCloser)
   401  	if err != nil {
   402  		return nil, nil, 0, "", err
   403  	}
   404  
   405  	if err := dataTarballWriteCloser.Close(); err != nil {
   406  		return nil, nil, 0, "", fmt.Errorf("closing data tarball: %w", err)
   407  	}
   408  
   409  	return dataTarball.Bytes(), md5sums, instSize, name, nil
   410  }
   411  
   412  func fillDataTar(info *nfpm.Info, w io.Writer) (md5sums []byte, instSize int64, err error) {
   413  	out := tar.NewWriter(w)
   414  
   415  	// the writer is properly closed later, this is just in case that we have
   416  	// an error in another part of the code.
   417  	defer out.Close() // nolint: errcheck
   418  
   419  	md5buf, instSize, err := createFilesInsideDataTar(info, out)
   420  	if err != nil {
   421  		return nil, 0, err
   422  	}
   423  
   424  	if err := out.Close(); err != nil {
   425  		return nil, 0, fmt.Errorf("closing data.tar.gz: %w", err)
   426  	}
   427  
   428  	return md5buf.Bytes(), instSize, nil
   429  }
   430  
   431  func createFilesInsideDataTar(info *nfpm.Info, tw *tar.Writer) (md5buf bytes.Buffer, instSize int64, err error) {
   432  	for _, file := range info.Contents {
   433  		switch file.Type {
   434  		case files.TypeRPMGhost:
   435  			continue // skip ghost files in deb
   436  		case files.TypeDir, files.TypeImplicitDir:
   437  			header, err := tarHeader(file, info.MTime)
   438  			if err != nil {
   439  				return md5buf, 0, fmt.Errorf("build directory header for %q: %w",
   440  					file.Destination, err)
   441  			}
   442  
   443  			err = tw.WriteHeader(header)
   444  			if err != nil {
   445  				return md5buf, 0, fmt.Errorf("create directory %q in data tar: %w",
   446  					header.Name, err)
   447  			}
   448  		case files.TypeSymlink:
   449  			header, err := tarHeader(file, info.MTime)
   450  			if err != nil {
   451  				return md5buf, 0, fmt.Errorf("build symlink header for %q: %w",
   452  					file.Destination, err)
   453  			}
   454  
   455  			err = newItemInsideTar(tw, []byte{}, header)
   456  			if err != nil {
   457  				return md5buf, 0, fmt.Errorf("create symlink %q in data tar: %w",
   458  					header.Linkname, err)
   459  			}
   460  		case files.TypeDebChangelog:
   461  			size, err := createChangelogInsideDataTar(tw, &md5buf, info, file.Destination)
   462  			if err != nil {
   463  				return md5buf, 0, fmt.Errorf("write changelog to data tar: %w", err)
   464  			}
   465  
   466  			instSize += size
   467  		default:
   468  			size, err := copyToTarAndDigest(file, tw, &md5buf)
   469  			if err != nil {
   470  				return md5buf, 0, fmt.Errorf("write %q to data tar: %w", file.Destination, err)
   471  			}
   472  
   473  			instSize += size
   474  		}
   475  	}
   476  
   477  	return md5buf, instSize, nil
   478  }
   479  
   480  func copyToTarAndDigest(file *files.Content, tw *tar.Writer, md5w io.Writer) (int64, error) {
   481  	tarFile, err := os.OpenFile(file.Source, os.O_RDONLY, 0o600) //nolint:gosec
   482  	if err != nil {
   483  		return 0, fmt.Errorf("could not add tarFile to the archive: %w", err)
   484  	}
   485  	// don't care if it errs while closing...
   486  	defer tarFile.Close() // nolint: errcheck,gosec
   487  
   488  	header, err := tarHeader(file)
   489  	if err != nil {
   490  		return 0, err
   491  	}
   492  
   493  	if err := tw.WriteHeader(header); err != nil {
   494  		return 0, fmt.Errorf("cannot write header of %s to data.tar.gz: %w", file.Source, err)
   495  	}
   496  	digest := md5.New() // nolint:gas
   497  	if _, err := io.Copy(tw, io.TeeReader(tarFile, digest)); err != nil {
   498  		return 0, fmt.Errorf("%s: failed to copy: %w", file.Source, err)
   499  	}
   500  	if _, err := fmt.Fprintf(md5w, "%x  %s\n", digest.Sum(nil), header.Name); err != nil {
   501  		return 0, fmt.Errorf("%s: failed to write md5: %w", file.Source, err)
   502  	}
   503  	return file.Size(), nil
   504  }
   505  
   506  func withChangelogIfRequested(info *nfpm.Info) *nfpm.Info {
   507  	if info.Changelog == "" {
   508  		return info
   509  	}
   510  
   511  	// https://www.debian.org/doc/manuals/developers-reference/pkgs.de.html#recording-changes-in-the-package
   512  	// https://lintian.debian.org/tags/debian-changelog-file-missing-or-wrong-name
   513  	info.Contents = append(info.Contents, &files.Content{
   514  		Destination: fmt.Sprintf("/usr/share/doc/%s/changelog.Debian.gz", info.Name),
   515  		Type:        files.TypeDebChangelog, // this type is handeled in createDataTarball
   516  	})
   517  
   518  	return info
   519  }
   520  
   521  func createChangelogInsideDataTar(
   522  	tarw *tar.Writer,
   523  	g io.Writer,
   524  	info *nfpm.Info,
   525  	fileName string,
   526  ) (int64, error) {
   527  	var buf bytes.Buffer
   528  	// we need here a non timestamped compression -> https://github.com/klauspost/pgzip doesn't support that
   529  	// https://github.com/klauspost/pgzip/blob/v1.2.6/gzip.go#L322 vs.
   530  	// https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/compress/gzip/gzip.go;l=157
   531  	out, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
   532  	if err != nil {
   533  		return 0, fmt.Errorf("could not create gzip writer: %w", err)
   534  	}
   535  	// the writers are properly closed later, this is just in case that we have
   536  	// an error in another part of the code.
   537  	defer out.Close() // nolint: errcheck
   538  
   539  	changelogContent, err := formatChangelog(info)
   540  	if err != nil {
   541  		return 0, err
   542  	}
   543  
   544  	if _, err = io.WriteString(out, changelogContent); err != nil {
   545  		return 0, err
   546  	}
   547  
   548  	if err = out.Close(); err != nil {
   549  		return 0, fmt.Errorf("closing %s: %w", filepath.Base(fileName), err)
   550  	}
   551  
   552  	changelogData := buf.Bytes()
   553  
   554  	digest := md5.New() // nolint:gas
   555  	if _, err = digest.Write(changelogData); err != nil {
   556  		return 0, err
   557  	}
   558  
   559  	if _, err = fmt.Fprintf(
   560  		g,
   561  		"%x  %s\n",
   562  		digest.Sum(nil),
   563  		files.AsExplicitRelativePath(fileName),
   564  	); err != nil {
   565  		return 0, err
   566  	}
   567  
   568  	if err = newFileInsideTar(tarw, fileName, changelogData, modtime.Get(info.MTime)); err != nil {
   569  		return 0, err
   570  	}
   571  
   572  	return int64(len(changelogData)), nil
   573  }
   574  
   575  func formatChangelog(info *nfpm.Info) (string, error) {
   576  	changelog, err := info.GetChangeLog()
   577  	if err != nil {
   578  		return "", err
   579  	}
   580  
   581  	tpl, err := chglog.DebTemplate()
   582  	if err != nil {
   583  		return "", err
   584  	}
   585  
   586  	formattedChangelog, err := chglog.FormatChangelog(changelog, tpl)
   587  	if err != nil {
   588  		return "", err
   589  	}
   590  
   591  	return strings.TrimSpace(formattedChangelog) + "\n", nil
   592  }
   593  
   594  // nolint:funlen
   595  func createControl(instSize int64, md5sums []byte, info *nfpm.Info) (controlTarGz []byte, err error) {
   596  	var buf bytes.Buffer
   597  	compress := gzip.NewWriter(&buf)
   598  	out := tar.NewWriter(compress)
   599  	// the writers are properly closed later, this is just in case that we have
   600  	// an error in another part of the code.
   601  	defer out.Close()      // nolint: errcheck
   602  	defer compress.Close() // nolint: errcheck
   603  
   604  	var body bytes.Buffer
   605  	if err = writeControl(&body, controlData{
   606  		Info:          info,
   607  		InstalledSize: instSize / 1024,
   608  	}); err != nil {
   609  		return nil, err
   610  	}
   611  
   612  	mtime := modtime.Get(info.MTime)
   613  	if err := newFileInsideTar(out, "./control", body.Bytes(), mtime); err != nil {
   614  		return nil, err
   615  	}
   616  	if err := newFileInsideTar(out, "./md5sums", md5sums, mtime); err != nil {
   617  		return nil, err
   618  	}
   619  	if conffiles, ok := conffiles(info); ok {
   620  		if err := newFileInsideTar(out, "./conffiles", conffiles, mtime); err != nil {
   621  			return nil, err
   622  		}
   623  	}
   624  
   625  	if triggers := createTriggers(info); len(triggers) > 0 {
   626  		if err := newFileInsideTar(out, "./triggers", triggers, mtime); err != nil {
   627  			return nil, err
   628  		}
   629  	}
   630  
   631  	type fileAndMode struct {
   632  		fileName string
   633  		mode     int64
   634  	}
   635  
   636  	specialFiles := map[string]*fileAndMode{
   637  		"preinst": {
   638  			fileName: info.Scripts.PreInstall,
   639  			mode:     0o755,
   640  		},
   641  		"postinst": {
   642  			fileName: info.Scripts.PostInstall,
   643  			mode:     0o755,
   644  		},
   645  		"prerm": {
   646  			fileName: info.Scripts.PreRemove,
   647  			mode:     0o755,
   648  		},
   649  		"postrm": {
   650  			fileName: info.Scripts.PostRemove,
   651  			mode:     0o755,
   652  		},
   653  		"rules": {
   654  			fileName: info.Deb.Scripts.Rules,
   655  			mode:     0o755,
   656  		},
   657  		"templates": {
   658  			fileName: info.Deb.Scripts.Templates,
   659  			mode:     0o644,
   660  		},
   661  		"config": {
   662  			fileName: info.Deb.Scripts.Config,
   663  			mode:     0o755,
   664  		},
   665  	}
   666  
   667  	for _, filename := range maps.Keys(specialFiles) {
   668  		dets := specialFiles[filename]
   669  		if dets.fileName == "" {
   670  			continue
   671  		}
   672  		if err := newFilePathInsideTar(out, dets.fileName, filename, dets.mode, mtime); err != nil {
   673  			return nil, err
   674  		}
   675  	}
   676  
   677  	if err := out.Close(); err != nil {
   678  		return nil, fmt.Errorf("closing control.tar.gz: %w", err)
   679  	}
   680  	if err := compress.Close(); err != nil {
   681  		return nil, fmt.Errorf("closing control.tar.gz: %w", err)
   682  	}
   683  	return buf.Bytes(), nil
   684  }
   685  
   686  func newItemInsideTar(out *tar.Writer, content []byte, header *tar.Header) error {
   687  	if err := out.WriteHeader(header); err != nil {
   688  		return fmt.Errorf("cannot write header of %s file to control.tar.gz: %w", header.Name, err)
   689  	}
   690  	if _, err := out.Write(content); err != nil {
   691  		return fmt.Errorf("cannot write %s file to control.tar.gz: %w", header.Name, err)
   692  	}
   693  	return nil
   694  }
   695  
   696  func newFileInsideTar(out *tar.Writer, name string, content []byte, modtime time.Time) error {
   697  	return newItemInsideTar(out, content, &tar.Header{
   698  		Name:     files.AsExplicitRelativePath(name),
   699  		Size:     int64(len(content)),
   700  		Mode:     0o644,
   701  		ModTime:  modtime,
   702  		Typeflag: tar.TypeReg,
   703  		Format:   tar.FormatGNU,
   704  	})
   705  }
   706  
   707  func newFilePathInsideTar(out *tar.Writer, path, dest string, mode int64, modtime time.Time) error {
   708  	content, err := os.ReadFile(path)
   709  	if err != nil {
   710  		return err
   711  	}
   712  	return newItemInsideTar(out, content, &tar.Header{
   713  		Name:     files.AsExplicitRelativePath(dest),
   714  		Size:     int64(len(content)),
   715  		Mode:     mode,
   716  		ModTime:  modtime,
   717  		Typeflag: tar.TypeReg,
   718  		Format:   tar.FormatGNU,
   719  	})
   720  }
   721  
   722  func conffiles(info *nfpm.Info) ([]byte, bool) {
   723  	// nolint: prealloc
   724  	var confs []string
   725  	for _, file := range info.Contents {
   726  		switch file.Type {
   727  		case files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK:
   728  			confs = append(confs, files.NormalizeAbsoluteFilePath(file.Destination))
   729  		}
   730  	}
   731  	if len(confs) == 0 {
   732  		return nil, false
   733  	}
   734  
   735  	return []byte(strings.Join(confs, "\n") + "\n"), true
   736  }
   737  
   738  func createTriggers(info *nfpm.Info) []byte {
   739  	var buffer bytes.Buffer
   740  
   741  	// https://man7.org/linux/man-pages/man5/deb-triggers.5.html
   742  	triggerEntries := []struct {
   743  		Directive    string
   744  		TriggerNames *[]string
   745  	}{
   746  		{"interest", &info.Deb.Triggers.Interest},
   747  		{"interest-await", &info.Deb.Triggers.InterestAwait},
   748  		{"interest-noawait", &info.Deb.Triggers.InterestNoAwait},
   749  		{"activate", &info.Deb.Triggers.Activate},
   750  		{"activate-await", &info.Deb.Triggers.ActivateAwait},
   751  		{"activate-noawait", &info.Deb.Triggers.ActivateNoAwait},
   752  	}
   753  
   754  	for _, triggerEntry := range triggerEntries {
   755  		for _, triggerName := range *triggerEntry.TriggerNames {
   756  			fmt.Fprintf(&buffer, "%s %s\n", triggerEntry.Directive, triggerName)
   757  		}
   758  	}
   759  
   760  	return buffer.Bytes()
   761  }
   762  
   763  const controlTemplate = `
   764  {{- /* Mandatory fields */ -}}
   765  Package: {{.Info.Name}}
   766  Version: {{ if .Info.Epoch}}{{ .Info.Epoch }}:{{ end }}{{.Info.Version}}
   767           {{- if .Info.Prerelease}}~{{ .Info.Prerelease }}{{- end }}
   768           {{- if .Info.VersionMetadata}}+{{ .Info.VersionMetadata }}{{- end }}
   769           {{- if .Info.Release}}-{{ .Info.Release }}{{- end }}
   770  Section: {{.Info.Section}}
   771  Priority: {{.Info.Priority}}
   772  Architecture: {{ if ne .Info.Platform "linux"}}{{ .Info.Platform }}-{{ end }}{{.Info.Arch}}
   773  {{- /* Optional fields */ -}}
   774  {{- if .Info.License }}
   775  License: {{.Info.License}}
   776  {{- end }}
   777  {{- if .Info.Maintainer}}
   778  Maintainer: {{.Info.Maintainer}}
   779  {{- end }}
   780  Installed-Size: {{.InstalledSize}}
   781  {{- with .Info.Replaces}}
   782  Replaces: {{join .}}
   783  {{- end }}
   784  {{- with nonEmpty .Info.Provides}}
   785  Provides: {{join .}}
   786  {{- end }}
   787  {{- with .Info.Deb.Predepends}}
   788  Pre-Depends: {{join .}}
   789  {{- end }}
   790  {{- with .Info.Depends}}
   791  Depends: {{join .}}
   792  {{- end }}
   793  {{- with .Info.Recommends}}
   794  Recommends: {{join .}}
   795  {{- end }}
   796  {{- with .Info.Suggests}}
   797  Suggests: {{join .}}
   798  {{- end }}
   799  {{- with .Info.Conflicts}}
   800  Conflicts: {{join .}}
   801  {{- end }}
   802  {{- with .Info.Deb.Breaks}}
   803  Breaks: {{join .}}
   804  {{- end }}
   805  {{- if .Info.Homepage}}
   806  Homepage: {{.Info.Homepage}}
   807  {{- end }}
   808  {{- /* Mandatory fields */}}
   809  Description: {{multiline .Info.Description}}
   810  {{- range $key, $value := .Info.Deb.Fields }}
   811  {{- if $value }}
   812  {{$key}}: {{$value}}
   813  {{- end }}
   814  {{- end }}
   815  `
   816  
   817  type controlData struct {
   818  	Info          *nfpm.Info
   819  	InstalledSize int64
   820  }
   821  
   822  func writeControl(w io.Writer, data controlData) error {
   823  	tmpl := template.New("control")
   824  	tmpl.Funcs(template.FuncMap{
   825  		"join": func(strs []string) string {
   826  			return strings.Trim(strings.Join(strs, ", "), " ")
   827  		},
   828  		"multiline": func(strs string) string {
   829  			var b strings.Builder
   830  			s := bufio.NewScanner(strings.NewReader(strings.TrimSpace(strs)))
   831  			s.Scan()
   832  			b.Write(bytes.TrimSpace(s.Bytes()))
   833  			for s.Scan() {
   834  				b.WriteString("\n ")
   835  				l := bytes.TrimSpace(s.Bytes())
   836  				if len(l) == 0 {
   837  					b.WriteByte('.')
   838  				} else {
   839  					b.Write(l)
   840  				}
   841  			}
   842  			return b.String()
   843  		},
   844  		"nonEmpty": func(strs []string) []string {
   845  			var result []string
   846  			for _, s := range strs {
   847  				s := strings.TrimSpace(s)
   848  				if s == "" {
   849  					continue
   850  				}
   851  				result = append(result, s)
   852  			}
   853  			return result
   854  		},
   855  	})
   856  	return template.Must(tmpl.Parse(controlTemplate)).Execute(w, data)
   857  }
   858  
   859  func tarHeader(content *files.Content, preferredModTimes ...time.Time) (*tar.Header, error) {
   860  	const (
   861  		ISUID = 0o4000 // Set uid
   862  		ISGID = 0o2000 // Set gid
   863  		ISVTX = 0o1000 // Save text (sticky bit)
   864  	)
   865  
   866  	fm := content.Mode()
   867  
   868  	h := &tar.Header{
   869  		Name: content.Name(),
   870  		ModTime: modtime.Get(
   871  			append(preferredModTimes, content.ModTime())...),
   872  		Mode:   int64(fm & 0o7777),
   873  		Uname:  content.FileInfo.Owner,
   874  		Gname:  content.FileInfo.Group,
   875  		Format: tar.FormatGNU,
   876  	}
   877  
   878  	switch {
   879  	case content.IsDir() || fm&fs.ModeDir != 0:
   880  		h.Typeflag = tar.TypeDir
   881  		h.Name = files.AsExplicitRelativePath(content.Destination)
   882  	case content.Type == files.TypeSymlink || fm&fs.ModeSymlink != 0:
   883  		h.Typeflag = tar.TypeSymlink
   884  		h.Name = files.AsExplicitRelativePath(content.Destination)
   885  		h.Linkname = content.Source
   886  	case fm&fs.ModeDevice != 0:
   887  		if fm&fs.ModeCharDevice != 0 {
   888  			h.Typeflag = tar.TypeChar
   889  		} else {
   890  			h.Typeflag = tar.TypeBlock
   891  		}
   892  	case fm&fs.ModeNamedPipe != 0:
   893  		h.Typeflag = tar.TypeFifo
   894  	case fm&fs.ModeSocket != 0:
   895  		return nil, fmt.Errorf("archive/tar: sockets not supported")
   896  	default:
   897  		h.Typeflag = tar.TypeReg
   898  		h.Name = files.AsExplicitRelativePath(content.Destination)
   899  		h.Size = content.Size()
   900  	}
   901  
   902  	if fm&fs.ModeSetuid != 0 {
   903  		h.Mode |= ISUID
   904  	}
   905  	if fm&fs.ModeSetgid != 0 {
   906  		h.Mode |= ISGID
   907  	}
   908  	if fm&fs.ModeSticky != 0 {
   909  		h.Mode |= ISVTX
   910  	}
   911  
   912  	return h, nil
   913  }