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

     1  /*
     2  Copyright 2019 Torsten Curdt
     3  
     4  Permission is hereby granted, free of charge, to any person obtaining a copy
     5  of this software and associated documentation files (the "Software"), to deal
     6  in the Software without restriction, including without limitation the rights
     7  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     8  copies of the Software, and to permit persons to whom the Software is
     9  furnished to do so, subject to the following conditions:
    10  
    11  The above copyright notice and this permission notice shall be included in all
    12  copies or substantial portions of the Software.
    13  
    14  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    15  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    16  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    17  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    18  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    19  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    20  SOFTWARE.
    21  */
    22  
    23  // Package apk implements nfpm.Packager providing .apk bindings.
    24  package apk
    25  
    26  // Initial implementation from https://gist.github.com/tcurdt/512beaac7e9c12dcf5b6b7603b09d0d8
    27  
    28  import (
    29  	"archive/tar"
    30  	"bufio"
    31  	"bytes"
    32  	"crypto/sha1"
    33  	"crypto/sha256"
    34  	"encoding/hex"
    35  	"errors"
    36  	"fmt"
    37  	"hash"
    38  	"io"
    39  	"net/mail"
    40  	"os"
    41  	"strings"
    42  	"sync/atomic"
    43  	"text/template"
    44  	"time"
    45  
    46  	"github.com/goreleaser/nfpm/v2"
    47  	"github.com/goreleaser/nfpm/v2/files"
    48  	"github.com/goreleaser/nfpm/v2/internal/maps"
    49  	"github.com/goreleaser/nfpm/v2/internal/sign"
    50  	gzip "github.com/klauspost/pgzip"
    51  )
    52  
    53  const packagerName = "apk"
    54  
    55  // nolint: gochecknoinits
    56  func init() {
    57  	nfpm.RegisterPackager(packagerName, Default)
    58  }
    59  
    60  // https://wiki.alpinelinux.org/wiki/Architecture
    61  // nolint: gochecknoglobals
    62  var archToAlpine = map[string]string{
    63  	"386":     "x86",
    64  	"amd64":   "x86_64",
    65  	"arm64":   "aarch64",
    66  	"arm6":    "armhf",
    67  	"arm7":    "armv7",
    68  	"ppc64le": "ppc64le",
    69  	"s390":    "s390x",
    70  }
    71  
    72  func ensureValidArch(info *nfpm.Info) *nfpm.Info {
    73  	if info.APK.Arch != "" {
    74  		info.Arch = info.APK.Arch
    75  	} else if arch, ok := archToAlpine[info.Arch]; ok {
    76  		info.Arch = arch
    77  	}
    78  
    79  	return info
    80  }
    81  
    82  // Default apk packager.
    83  // nolint: gochecknoglobals
    84  var Default = &Apk{}
    85  
    86  // Apk is an apk packager implementation.
    87  type Apk struct{}
    88  
    89  func (a *Apk) ConventionalFileName(info *nfpm.Info) string {
    90  	info = ensureValidArch(info)
    91  	version := pkgver(info)
    92  	return fmt.Sprintf("%s_%s_%s.apk", info.Name, version, info.Arch)
    93  }
    94  
    95  // ConventionalExtension returns the file name conventionally used for Apk packages
    96  func (*Apk) ConventionalExtension() string {
    97  	return ".apk"
    98  }
    99  
   100  // Package writes a new apk package to the given writer using the given info.
   101  func (*Apk) Package(info *nfpm.Info, apk io.Writer) (err error) {
   102  	if info.Platform != "linux" {
   103  		return fmt.Errorf("invalid platform: %s", info.Platform)
   104  	}
   105  	info = ensureValidArch(info)
   106  
   107  	if err := nfpm.PrepareForPackager(info, packagerName); err != nil {
   108  		return err
   109  	}
   110  
   111  	var bufData bytes.Buffer
   112  
   113  	size := int64(0)
   114  	// create the data tgz
   115  	dataDigest, err := createData(&bufData, info, &size)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	// create the control tgz
   121  	var bufControl bytes.Buffer
   122  	controlDigest, err := createControl(&bufControl, info, size, dataDigest)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	if info.APK.Signature.KeyFile == "" && info.APK.Signature.SignFn == nil {
   128  		return combineToApk(apk, &bufControl, &bufData)
   129  	}
   130  
   131  	// create the signature tgz
   132  	var bufSignature bytes.Buffer
   133  	if err = createSignature(&bufSignature, info, controlDigest); err != nil {
   134  		return err
   135  	}
   136  
   137  	return combineToApk(apk, &bufSignature, &bufControl, &bufData)
   138  }
   139  
   140  type writerCounter struct {
   141  	io.Writer
   142  	count  uint64
   143  	writer io.Writer
   144  }
   145  
   146  func newWriterCounter(w io.Writer) *writerCounter {
   147  	return &writerCounter{
   148  		writer: w,
   149  	}
   150  }
   151  
   152  func (counter *writerCounter) Write(buf []byte) (int, error) {
   153  	n, err := counter.writer.Write(buf)
   154  	atomic.AddUint64(&counter.count, uint64(n))
   155  	return n, err
   156  }
   157  
   158  func (counter *writerCounter) Count() uint64 {
   159  	return atomic.LoadUint64(&counter.count)
   160  }
   161  
   162  func writeFile(tw *tar.Writer, header *tar.Header, file io.Reader) error {
   163  	header.Format = tar.FormatUSTAR
   164  	header.ChangeTime = time.Time{}
   165  	header.AccessTime = time.Time{}
   166  
   167  	err := tw.WriteHeader(header)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	_, err = io.Copy(tw, file)
   173  	return err
   174  }
   175  
   176  type tarKind int
   177  
   178  const (
   179  	tarFull tarKind = iota
   180  	tarCut
   181  )
   182  
   183  func writeTgz(w io.Writer, kind tarKind, builder func(tw *tar.Writer) error, digest hash.Hash) ([]byte, error) {
   184  	mw := io.MultiWriter(digest, w)
   185  	gw := gzip.NewWriter(mw)
   186  	cw := newWriterCounter(gw)
   187  	bw := bufio.NewWriterSize(cw, 4096)
   188  	tw := tar.NewWriter(bw)
   189  
   190  	err := builder(tw)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	// handle the cut vs full tars
   196  	// TODO: document this better, why do we need to call bw.Flush twice if it is a full tar vs the cut tar?
   197  	if err = bw.Flush(); err != nil {
   198  		return nil, err
   199  	}
   200  	if err = tw.Close(); err != nil {
   201  		return nil, err
   202  	}
   203  	if kind == tarFull {
   204  		if err = bw.Flush(); err != nil {
   205  			return nil, err
   206  		}
   207  	}
   208  
   209  	size := cw.Count()
   210  	alignedSize := (size + 511) & ^uint64(511)
   211  
   212  	increase := alignedSize - size
   213  	if increase > 0 {
   214  		b := make([]byte, increase)
   215  		_, err = cw.Write(b)
   216  		if err != nil {
   217  			return nil, err
   218  		}
   219  	}
   220  
   221  	if err = gw.Close(); err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	return digest.Sum(nil), nil
   226  }
   227  
   228  func createData(dataTgz io.Writer, info *nfpm.Info, sizep *int64) ([]byte, error) {
   229  	builderData := createBuilderData(info, sizep)
   230  	dataDigest, err := writeTgz(dataTgz, tarFull, builderData, sha256.New())
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	return dataDigest, nil
   235  }
   236  
   237  func createControl(controlTgz io.Writer, info *nfpm.Info, size int64, dataDigest []byte) ([]byte, error) {
   238  	builderControl := createBuilderControl(info, size, dataDigest)
   239  	controlDigest, err := writeTgz(controlTgz, tarCut, builderControl, sha1.New()) // nolint:gosec
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	return controlDigest, nil
   244  }
   245  
   246  func createSignature(signatureTgz io.Writer, info *nfpm.Info, controlSHA1Digest []byte) error {
   247  	signatureBuilder := createSignatureBuilder(controlSHA1Digest, info)
   248  	// we don't actually need to produce a digest here, but writeTgz
   249  	// requires it so we just use SHA1 since it is already imported
   250  	_, err := writeTgz(signatureTgz, tarCut, signatureBuilder, sha1.New()) // nolint:gosec
   251  	if err != nil {
   252  		return &nfpm.ErrSigningFailure{Err: err}
   253  	}
   254  
   255  	return nil
   256  }
   257  
   258  var errNoKeyAddress = errors.New("key name not set and maintainer mail address empty")
   259  
   260  func createSignatureBuilder(digest []byte, info *nfpm.Info) func(*tar.Writer) error {
   261  	return func(tw *tar.Writer) error {
   262  		var signature []byte
   263  		var err error
   264  		if signFn := info.APK.Signature.SignFn; signFn != nil {
   265  			signature, err = signFn(bytes.NewReader(digest))
   266  		} else {
   267  			signature, err = sign.RSASignSHA1Digest(digest,
   268  				info.APK.Signature.KeyFile, info.APK.Signature.KeyPassphrase)
   269  		}
   270  		if err != nil {
   271  			return err
   272  		}
   273  
   274  		// needs to exist on the machine during installation: /etc/apk/keys/<keyname>.rsa.pub
   275  		keyname := info.APK.Signature.KeyName
   276  		if keyname == "" {
   277  			addr, err := mail.ParseAddress(info.Maintainer)
   278  			if err != nil {
   279  				return fmt.Errorf("key name not set and unable to parse maintainer mail address: %w", err)
   280  			} else if addr.Address == "" {
   281  				return errNoKeyAddress
   282  			}
   283  
   284  			keyname = addr.Address
   285  		}
   286  		if !strings.HasSuffix(keyname, ".rsa.pub") {
   287  			keyname += ".rsa.pub"
   288  		}
   289  
   290  		// In principle apk supports RSA signatures over SHA256/512 keys, but in
   291  		// practice verification works but installation segfaults. If this is
   292  		// fixed at some point we should also upgrade the hash. In this case,
   293  		// the file name will have to start with .SIGN.RSA256 or .SIGN.RSA512.
   294  		signHeader := &tar.Header{
   295  			Name: fmt.Sprintf(".SIGN.RSA.%s", keyname),
   296  			Mode: 0o600,
   297  			Size: int64(len(signature)),
   298  		}
   299  
   300  		return writeFile(tw, signHeader, bytes.NewReader(signature))
   301  	}
   302  }
   303  
   304  func combineToApk(target io.Writer, readers ...io.Reader) error {
   305  	for _, tgz := range readers {
   306  		if _, err := io.Copy(target, tgz); err != nil {
   307  			return err
   308  		}
   309  	}
   310  	return nil
   311  }
   312  
   313  func createBuilderControl(info *nfpm.Info, size int64, dataDigest []byte) func(tw *tar.Writer) error {
   314  	return func(tw *tar.Writer) error {
   315  		var infoBuf bytes.Buffer
   316  		if err := writeControl(&infoBuf, controlData{
   317  			Info:          info,
   318  			InstalledSize: size,
   319  			Datahash:      hex.EncodeToString(dataDigest),
   320  		}); err != nil {
   321  			return err
   322  		}
   323  		infoContent := infoBuf.String()
   324  
   325  		infoHeader := &tar.Header{
   326  			Name: ".PKGINFO",
   327  			Mode: 0o600,
   328  			Size: int64(len(infoContent)),
   329  		}
   330  
   331  		if err := writeFile(tw, infoHeader, strings.NewReader(infoContent)); err != nil {
   332  			return err
   333  		}
   334  
   335  		// NOTE: Apk scripts tend to follow the pattern:
   336  		// #!/bin/sh
   337  		//
   338  		// bin/echo 'running preinstall.sh' // do stuff here
   339  		//
   340  		// exit 0
   341  		scripts := map[string]string{
   342  			".pre-install":    info.Scripts.PreInstall,
   343  			".pre-upgrade":    info.APK.Scripts.PreUpgrade,
   344  			".post-install":   info.Scripts.PostInstall,
   345  			".post-upgrade":   info.APK.Scripts.PostUpgrade,
   346  			".pre-deinstall":  info.Scripts.PreRemove,
   347  			".post-deinstall": info.Scripts.PostRemove,
   348  		}
   349  		for _, name := range maps.Keys(scripts) {
   350  			path := scripts[name]
   351  			if path == "" {
   352  				continue
   353  			}
   354  			if err := newScriptInsideTarGz(tw, path, name); err != nil {
   355  				return err
   356  			}
   357  		}
   358  
   359  		return nil
   360  	}
   361  }
   362  
   363  func newScriptInsideTarGz(out *tar.Writer, path, dest string) error {
   364  	file, err := os.Stat(path) //nolint:gosec
   365  	if err != nil {
   366  		return err
   367  	}
   368  	content, err := os.ReadFile(path)
   369  	if err != nil {
   370  		return err
   371  	}
   372  	return newItemInsideTarGz(out, content, &tar.Header{
   373  		Name:     files.ToNixPath(dest),
   374  		Size:     int64(len(content)),
   375  		Mode:     0o755,
   376  		ModTime:  file.ModTime(),
   377  		Typeflag: tar.TypeReg,
   378  	})
   379  }
   380  
   381  func newItemInsideTarGz(out *tar.Writer, content []byte, header *tar.Header) error {
   382  	header.Format = tar.FormatPAX
   383  	header.PAXRecords = make(map[string]string)
   384  
   385  	hasher := sha1.New()
   386  	_, err := hasher.Write(content)
   387  	if err != nil {
   388  		return fmt.Errorf("failed to hash content of file %s: %w", header.Name, err)
   389  	}
   390  	header.PAXRecords["APK-TOOLS.checksum.SHA1"] = fmt.Sprintf("%x", hasher.Sum(nil))
   391  	if err := out.WriteHeader(header); err != nil {
   392  		return fmt.Errorf("cannot write header of %s file to apk: %w", header.Name, err)
   393  	}
   394  	if _, err := out.Write(content); err != nil {
   395  		return fmt.Errorf("cannot write %s file to apk: %w", header.Name, err)
   396  	}
   397  	return nil
   398  }
   399  
   400  func createBuilderData(info *nfpm.Info, sizep *int64) func(tw *tar.Writer) error {
   401  	return func(tw *tar.Writer) error {
   402  		return createFilesInsideTarGz(info, tw, sizep)
   403  	}
   404  }
   405  
   406  func createFilesInsideTarGz(info *nfpm.Info, tw *tar.Writer, sizep *int64) (err error) {
   407  	for _, file := range info.Contents {
   408  		file.Destination = files.AsRelativePath(file.Destination)
   409  
   410  		switch file.Type {
   411  		case files.TypeDir, files.TypeImplicitDir:
   412  			err = tw.WriteHeader(&tar.Header{
   413  				Name:     file.Destination,
   414  				Mode:     int64(file.FileInfo.Mode),
   415  				Typeflag: tar.TypeDir,
   416  				Uname:    file.FileInfo.Owner,
   417  				Gname:    file.FileInfo.Group,
   418  				ModTime:  file.FileInfo.MTime,
   419  			})
   420  		case files.TypeSymlink:
   421  			err = newItemInsideTarGz(tw, []byte{}, &tar.Header{
   422  				Name:     file.Destination,
   423  				Linkname: file.Source,
   424  				Typeflag: tar.TypeSymlink,
   425  				ModTime:  file.FileInfo.MTime,
   426  			})
   427  		default:
   428  			err = copyToTarAndDigest(file, tw, sizep)
   429  		}
   430  		if err != nil {
   431  			return err
   432  		}
   433  	}
   434  
   435  	return nil
   436  }
   437  
   438  func copyToTarAndDigest(file *files.Content, tw *tar.Writer, sizep *int64) error {
   439  	contents, err := os.ReadFile(file.Source)
   440  	if err != nil {
   441  		return err
   442  	}
   443  	header, err := tar.FileInfoHeader(file, file.Source)
   444  	if err != nil {
   445  		return err
   446  	}
   447  
   448  	// tar.FileInfoHeader only uses file.Mode().Perm() which masks the mode with
   449  	// 0o777 which we don't want because we want to be able to set the suid bit.
   450  	header.Mode = int64(file.Mode())
   451  	header.Name = files.AsRelativePath(file.Destination)
   452  	header.Uname = file.FileInfo.Owner
   453  	header.Gname = file.FileInfo.Group
   454  	if err = newItemInsideTarGz(tw, contents, header); err != nil {
   455  		return err
   456  	}
   457  
   458  	*sizep += file.Size()
   459  	return nil
   460  }
   461  
   462  // reference: https://wiki.adelielinux.org/wiki/APK_internals#.PKGINFO
   463  const controlTemplate = `
   464  {{- /* Mandatory fields */ -}}
   465  pkgname = {{.Info.Name}}
   466  pkgver = {{ pkgver .Info }}
   467  arch = {{.Info.Arch}}
   468  size = {{.InstalledSize}}
   469  pkgdesc = {{multiline .Info.Description}}
   470  {{- if .Info.Homepage}}
   471  url = {{.Info.Homepage}}
   472  {{- end }}
   473  {{- if .Info.Maintainer}}
   474  maintainer = {{.Info.Maintainer}}
   475  {{- end }}
   476  {{- range $repl := .Info.Replaces}}
   477  replaces = {{ $repl }}
   478  {{- end }}
   479  {{- range $prov := .Info.Provides}}
   480  provides = {{ $prov }}
   481  {{- end }}
   482  {{- range $dep := .Info.Depends}}
   483  depend = {{ $dep }}
   484  {{- end }}
   485  {{- if .Info.License}}
   486  license = {{.Info.License}}
   487  {{- end }}
   488  datahash = {{.Datahash}}
   489  `
   490  
   491  type controlData struct {
   492  	Info          *nfpm.Info
   493  	InstalledSize int64
   494  	Datahash      string
   495  }
   496  
   497  func writeControl(w io.Writer, data controlData) error {
   498  	tmpl := template.New("control")
   499  	tmpl.Funcs(template.FuncMap{
   500  		"multiline": func(strs string) string {
   501  			ret := strings.ReplaceAll(strs, "\n", "\n  ")
   502  			return strings.Trim(ret, " \n")
   503  		},
   504  		"pkgver": pkgver,
   505  	})
   506  	return template.Must(tmpl.Parse(controlTemplate)).Execute(w, data)
   507  }
   508  
   509  func pkgver(info *nfpm.Info) string {
   510  	version := info.Version
   511  
   512  	if info.Prerelease != "" {
   513  		version += "_" + info.Prerelease
   514  	}
   515  
   516  	if rel := info.Release; rel != "" {
   517  		if !strings.HasPrefix(rel, "r") {
   518  			rel = "r" + rel
   519  		}
   520  		version += "-" + rel
   521  	}
   522  	if meta := info.VersionMetadata; meta != "" {
   523  		if !strings.HasPrefix(meta, "p") &&
   524  			!strings.HasPrefix(meta, "cvs") &&
   525  			!strings.HasPrefix(meta, "svn") &&
   526  			!strings.HasPrefix(meta, "git") &&
   527  			!strings.HasPrefix(meta, "hg") {
   528  			meta = "p" + meta
   529  		}
   530  		version += "-" + meta
   531  	}
   532  	return version
   533  }