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

     1  // Package arch implements nfpm.Packager providing bindings for Arch Linux packages.
     2  package arch
     3  
     4  import (
     5  	"archive/tar"
     6  	"bytes"
     7  	"crypto/md5"
     8  	"crypto/sha256"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"slices"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/goreleaser/nfpm/v2"
    19  	"github.com/goreleaser/nfpm/v2/files"
    20  	"github.com/goreleaser/nfpm/v2/internal/maps"
    21  	"github.com/goreleaser/nfpm/v2/internal/modtime"
    22  	"github.com/klauspost/compress/zstd"
    23  	"github.com/klauspost/pgzip"
    24  )
    25  
    26  var ErrInvalidPkgName = errors.New("archlinux: package names may only contain alphanumeric characters or one of ., _, +, or -, and may not start with hyphen or dot")
    27  
    28  const packagerName = "archlinux"
    29  
    30  // nolint: gochecknoinits
    31  func init() {
    32  	nfpm.RegisterPackager(packagerName, Default)
    33  }
    34  
    35  // Default ArchLinux packager.
    36  // nolint: gochecknoglobals
    37  var Default = ArchLinux{}
    38  
    39  // ArchLinux packager.
    40  // nolint: revive
    41  type ArchLinux struct{}
    42  
    43  // nolint: gochecknoglobals
    44  var archToArchLinux = map[string]string{
    45  	"all":   "any",
    46  	"amd64": "x86_64",
    47  	"386":   "i686",
    48  	"arm64": "aarch64",
    49  	"arm7":  "armv7h",
    50  	"arm6":  "armv6h",
    51  	"arm5":  "arm",
    52  }
    53  
    54  func ensureValidArch(info *nfpm.Info) *nfpm.Info {
    55  	if info.ArchLinux.Arch != "" {
    56  		info.Arch = info.ArchLinux.Arch
    57  	} else if arch, ok := archToArchLinux[info.Arch]; ok {
    58  		info.Arch = arch
    59  	}
    60  
    61  	return info
    62  }
    63  
    64  // ConventionalFileName returns a file name for a package conforming
    65  // to Arch Linux package naming guidelines. See:
    66  // https://wiki.archlinux.org/title/Arch_package_guidelines#Package_naming
    67  func (ArchLinux) ConventionalFileName(info *nfpm.Info) string {
    68  	info = ensureValidArch(info)
    69  
    70  	pkgrel, err := strconv.Atoi(info.Release)
    71  	if err != nil {
    72  		pkgrel = 1
    73  	}
    74  
    75  	name := fmt.Sprintf(
    76  		"%s-%s-%d-%s.pkg.tar.zst",
    77  		info.Name,
    78  		info.Version+strings.ReplaceAll(info.Prerelease, "-", "_"),
    79  		pkgrel,
    80  		info.Arch,
    81  	)
    82  
    83  	return validPkgName(name)
    84  }
    85  
    86  // validPkgName removes any invalid characters from a string
    87  func validPkgName(s string) string {
    88  	s = strings.Map(mapValidChar, s)
    89  	s = strings.TrimLeft(s, "-.")
    90  	return s
    91  }
    92  
    93  // nameIsValid checks whether a package name is valid
    94  func nameIsValid(s string) bool {
    95  	return s != "" && s == validPkgName(s)
    96  }
    97  
    98  // mapValidChar returns r if it is allowed, otherwise, returns -1
    99  func mapValidChar(r rune) rune {
   100  	if r >= 'a' && r <= 'z' ||
   101  		r >= 'A' && r <= 'Z' ||
   102  		r >= '0' && r <= '9' ||
   103  		isOneOf(r, '.', '_', '+', '-') {
   104  		return r
   105  	}
   106  	return -1
   107  }
   108  
   109  // isOneOf checks whether a rune is one of the runes in rr
   110  func isOneOf(r rune, rr ...rune) bool {
   111  	return slices.Contains(rr, r)
   112  }
   113  
   114  // Package writes a new archlinux package to the given writer using the given info.
   115  func (ArchLinux) Package(info *nfpm.Info, w io.Writer) error {
   116  	if info.Platform != "linux" {
   117  		return fmt.Errorf("invalid platform: %s", info.Platform)
   118  	}
   119  	info = ensureValidArch(info)
   120  
   121  	err := nfpm.PrepareForPackager(info, packagerName)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	if !nameIsValid(info.Name) {
   127  		return ErrInvalidPkgName
   128  	}
   129  
   130  	zw, err := zstd.NewWriter(w)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	defer zw.Close()
   135  
   136  	tw := tar.NewWriter(zw)
   137  	defer tw.Close()
   138  
   139  	entries, totalSize, err := createFilesInTar(info, tw)
   140  	if err != nil {
   141  		return fmt.Errorf("create files in tar: %w", err)
   142  	}
   143  
   144  	pkginfoEntry, err := createPkginfo(info, tw, totalSize)
   145  	if err != nil {
   146  		return fmt.Errorf("create pkg info: %w", err)
   147  	}
   148  
   149  	// .PKGINFO must be the first entry in .MTREE
   150  	entries = append([]MtreeEntry{*pkginfoEntry}, entries...)
   151  
   152  	err = createMtree(tw, entries, modtime.Get(info.MTime))
   153  	if err != nil {
   154  		return fmt.Errorf("create mtree: %w", err)
   155  	}
   156  
   157  	return createScripts(info, tw)
   158  }
   159  
   160  // ConventionalExtension returns the file name conventionally used for Arch Linux packages
   161  func (ArchLinux) ConventionalExtension() string {
   162  	return ".pkg.tar.zst"
   163  }
   164  
   165  // createFilesInTar adds the files described in the given info to the given tar writer
   166  func createFilesInTar(info *nfpm.Info, tw *tar.Writer) ([]MtreeEntry, int64, error) {
   167  	entries := make([]MtreeEntry, 0, len(info.Contents))
   168  	var totalSize int64
   169  
   170  	for _, content := range info.Contents {
   171  		content.Destination = files.AsRelativePath(content.Destination)
   172  
   173  		switch content.Type {
   174  		case files.TypeDir, files.TypeImplicitDir:
   175  			entries = append(entries, MtreeEntry{
   176  				Destination: content.Destination,
   177  				Time:        content.ModTime().Unix(),
   178  				Mode:        int64(content.Mode()),
   179  				Type:        files.TypeDir,
   180  			})
   181  
   182  			if err := tw.WriteHeader(&tar.Header{
   183  				Name:     content.Destination,
   184  				Mode:     int64(content.Mode()),
   185  				Typeflag: tar.TypeDir,
   186  				ModTime:  content.ModTime(),
   187  				Uname:    content.FileInfo.Owner,
   188  				Gname:    content.FileInfo.Group,
   189  			}); err != nil {
   190  				return nil, 0, err
   191  			}
   192  		case files.TypeSymlink:
   193  			if err := tw.WriteHeader(&tar.Header{
   194  				Name:     content.Destination,
   195  				Linkname: content.Source,
   196  				ModTime:  content.ModTime(),
   197  				Typeflag: tar.TypeSymlink,
   198  			}); err != nil {
   199  				return nil, 0, err
   200  			}
   201  
   202  			entries = append(entries, MtreeEntry{
   203  				LinkSource:  content.Source,
   204  				Destination: content.Destination,
   205  				Time:        content.ModTime().Unix(),
   206  				Mode:        0o777,
   207  				Type:        content.Type,
   208  			})
   209  		default:
   210  			src, err := os.Open(content.Source)
   211  			if err != nil {
   212  				return nil, 0, err
   213  			}
   214  			defer src.Close() // nolint: errcheck
   215  
   216  			header := &tar.Header{
   217  				Name:     content.Destination,
   218  				Mode:     int64(content.Mode()),
   219  				Typeflag: tar.TypeReg,
   220  				Size:     content.Size(),
   221  				ModTime:  content.ModTime(),
   222  			}
   223  
   224  			if content.FileInfo != nil && content.Mode() != 0 {
   225  				header.Mode = int64(content.Mode())
   226  			}
   227  
   228  			if content.FileInfo != nil && !content.ModTime().IsZero() {
   229  				header.ModTime = content.ModTime()
   230  			}
   231  
   232  			if content.FileInfo != nil && content.Size() != 0 {
   233  				header.Size = content.Size()
   234  			}
   235  
   236  			err = tw.WriteHeader(header)
   237  			if err != nil {
   238  				return nil, 0, err
   239  			}
   240  
   241  			sha256Hash := sha256.New()
   242  			md5Hash := md5.New()
   243  
   244  			w := io.MultiWriter(tw, sha256Hash, md5Hash)
   245  
   246  			_, err = io.Copy(w, src)
   247  			if err != nil {
   248  				return nil, 0, err
   249  			}
   250  
   251  			entries = append(entries, MtreeEntry{
   252  				Destination: content.Destination,
   253  				Time:        content.ModTime().Unix(),
   254  				Mode:        int64(content.Mode()),
   255  				Size:        content.Size(),
   256  				Type:        content.Type,
   257  				MD5:         md5Hash.Sum(nil),
   258  				SHA256:      sha256Hash.Sum(nil),
   259  			})
   260  
   261  			totalSize += content.Size()
   262  		}
   263  	}
   264  
   265  	return entries, totalSize, nil
   266  }
   267  
   268  func defaultStr(s, def string) string {
   269  	if s == "" {
   270  		return def
   271  	}
   272  	return s
   273  }
   274  
   275  func createPkginfo(info *nfpm.Info, tw *tar.Writer, totalSize int64) (*MtreeEntry, error) {
   276  	if !nameIsValid(info.Name) {
   277  		return nil, ErrInvalidPkgName
   278  	}
   279  
   280  	buf := &bytes.Buffer{}
   281  
   282  	info = ensureValidArch(info)
   283  
   284  	pkgrel, err := strconv.Atoi(info.Release)
   285  	if err != nil {
   286  		pkgrel = 1
   287  	}
   288  
   289  	pkgver := fmt.Sprintf("%s-%d", info.Version, pkgrel)
   290  	if info.Epoch != "" {
   291  		epoch, err := strconv.ParseUint(info.Epoch, 10, 64)
   292  		if err == nil {
   293  			pkgver = fmt.Sprintf(
   294  				"%d:%s%s-%d",
   295  				epoch,
   296  				info.Version,
   297  				strings.ReplaceAll(info.Prerelease, "-", "_"),
   298  				pkgrel,
   299  			)
   300  		}
   301  	}
   302  
   303  	// Description cannot contain newlines
   304  	pkgdesc := strings.ReplaceAll(info.Description, "\n", " ")
   305  
   306  	_, err = io.WriteString(buf, "# Generated by nfpm\n")
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	builddate := strconv.FormatInt(modtime.Get(info.MTime).Unix(), 10)
   312  	totalSizeStr := strconv.FormatInt(totalSize, 10)
   313  
   314  	err = writeKVPairs(buf, map[string]string{
   315  		"size":      totalSizeStr,
   316  		"pkgname":   info.Name,
   317  		"pkgbase":   defaultStr(info.ArchLinux.Pkgbase, info.Name),
   318  		"pkgver":    pkgver,
   319  		"pkgdesc":   pkgdesc,
   320  		"url":       info.Homepage,
   321  		"builddate": builddate,
   322  		"packager":  defaultStr(info.ArchLinux.Packager, "Unknown Packager"),
   323  		"arch":      info.Arch,
   324  		"license":   info.License,
   325  	})
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  
   330  	for _, replaces := range info.Replaces {
   331  		err = writeKVPair(buf, "replaces", replaces)
   332  		if err != nil {
   333  			return nil, err
   334  		}
   335  	}
   336  
   337  	for _, conflict := range info.Conflicts {
   338  		err = writeKVPair(buf, "conflict", conflict)
   339  		if err != nil {
   340  			return nil, err
   341  		}
   342  	}
   343  
   344  	for _, provides := range info.Provides {
   345  		err = writeKVPair(buf, "provides", provides)
   346  		if err != nil {
   347  			return nil, err
   348  		}
   349  	}
   350  
   351  	for _, depend := range info.Depends {
   352  		err = writeKVPair(buf, "depend", depend)
   353  		if err != nil {
   354  			return nil, err
   355  		}
   356  	}
   357  
   358  	for _, content := range info.Contents {
   359  		if content.Type == files.TypeConfig || content.Type == files.TypeConfigNoReplace || content.Type == files.TypeConfigMissingOK {
   360  			path := files.AsRelativePath(content.Destination)
   361  
   362  			if err := writeKVPair(buf, "backup", path); err != nil {
   363  				return nil, err
   364  			}
   365  		}
   366  	}
   367  
   368  	size := buf.Len()
   369  
   370  	err = tw.WriteHeader(&tar.Header{
   371  		Typeflag: tar.TypeReg,
   372  		Mode:     0o644,
   373  		Name:     ".PKGINFO",
   374  		Size:     int64(size),
   375  		ModTime:  modtime.Get(info.MTime),
   376  	})
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	md5Hash := md5.New()
   382  	sha256Hash := sha256.New()
   383  
   384  	r := io.TeeReader(buf, md5Hash)
   385  	r = io.TeeReader(r, sha256Hash)
   386  
   387  	if _, err = io.Copy(tw, r); err != nil {
   388  		return nil, err
   389  	}
   390  
   391  	return &MtreeEntry{
   392  		Destination: ".PKGINFO",
   393  		Time:        modtime.Get(info.MTime).Unix(),
   394  		Mode:        0o644,
   395  		Size:        int64(size),
   396  		Type:        files.TypeFile,
   397  		MD5:         md5Hash.Sum(nil),
   398  		SHA256:      sha256Hash.Sum(nil),
   399  	}, nil
   400  }
   401  
   402  func writeKVPairs(w io.Writer, pairs map[string]string) error {
   403  	for _, key := range maps.Keys(pairs) {
   404  		if err := writeKVPair(w, key, pairs[key]); err != nil {
   405  			return err
   406  		}
   407  	}
   408  	return nil
   409  }
   410  
   411  func writeKVPair(w io.Writer, key, value string) error {
   412  	if value == "" {
   413  		return nil
   414  	}
   415  
   416  	_, err := io.WriteString(w, key)
   417  	if err != nil {
   418  		return err
   419  	}
   420  
   421  	_, err = io.WriteString(w, " = ")
   422  	if err != nil {
   423  		return err
   424  	}
   425  
   426  	_, err = io.WriteString(w, value)
   427  	if err != nil {
   428  		return err
   429  	}
   430  
   431  	_, err = io.WriteString(w, "\n")
   432  	return err
   433  }
   434  
   435  type MtreeEntry struct {
   436  	LinkSource  string
   437  	Destination string
   438  	Time        int64
   439  	Mode        int64
   440  	Size        int64
   441  	Type        string
   442  	MD5         []byte
   443  	SHA256      []byte
   444  }
   445  
   446  func (me *MtreeEntry) WriteTo(w io.Writer) (int64, error) {
   447  	switch me.Type {
   448  	case files.TypeDir, files.TypeImplicitDir:
   449  		n, err := fmt.Fprintf(
   450  			w,
   451  			"./%s time=%d.0 mode=%o type=dir\n",
   452  			me.Destination,
   453  			me.Time,
   454  			me.Mode,
   455  		)
   456  		return int64(n), err
   457  	case files.TypeSymlink:
   458  		n, err := fmt.Fprintf(
   459  			w,
   460  			"./%s time=%d.0 mode=%o type=link link=%s\n",
   461  			me.Destination,
   462  			me.Time,
   463  			me.Mode,
   464  			me.LinkSource,
   465  		)
   466  		return int64(n), err
   467  	default:
   468  		n, err := fmt.Fprintf(
   469  			w,
   470  			"./%s time=%d.0 mode=%o size=%d type=file md5digest=%x sha256digest=%x\n",
   471  			me.Destination,
   472  			me.Time,
   473  			me.Mode,
   474  			me.Size,
   475  			me.MD5,
   476  			me.SHA256,
   477  		)
   478  		return int64(n), err
   479  	}
   480  }
   481  
   482  func createMtree(tw *tar.Writer, entries []MtreeEntry, mtime time.Time) error {
   483  	buf := &bytes.Buffer{}
   484  	gw := pgzip.NewWriter(buf)
   485  	defer gw.Close()
   486  
   487  	_, err := io.WriteString(gw, "#mtree\n")
   488  	if err != nil {
   489  		return err
   490  	}
   491  
   492  	for _, entry := range entries {
   493  		_, err = entry.WriteTo(gw)
   494  		if err != nil {
   495  			return err
   496  		}
   497  	}
   498  
   499  	gw.Close()
   500  
   501  	err = tw.WriteHeader(&tar.Header{
   502  		Typeflag: tar.TypeReg,
   503  		Mode:     0o644,
   504  		Name:     ".MTREE",
   505  		Size:     int64(buf.Len()),
   506  		ModTime:  mtime,
   507  	})
   508  	if err != nil {
   509  		return err
   510  	}
   511  
   512  	_, err = io.Copy(tw, buf)
   513  	return err
   514  }
   515  
   516  func createScripts(info *nfpm.Info, tw *tar.Writer) error {
   517  	scripts := map[string]string{}
   518  
   519  	if info.Scripts.PreInstall != "" {
   520  		scripts["pre_install"] = info.Scripts.PreInstall
   521  	}
   522  
   523  	if info.Scripts.PostInstall != "" {
   524  		scripts["post_install"] = info.Scripts.PostInstall
   525  	}
   526  
   527  	if info.Scripts.PreRemove != "" {
   528  		scripts["pre_remove"] = info.Scripts.PreRemove
   529  	}
   530  
   531  	if info.Scripts.PostRemove != "" {
   532  		scripts["post_remove"] = info.Scripts.PostRemove
   533  	}
   534  
   535  	if info.ArchLinux.Scripts.PreUpgrade != "" {
   536  		scripts["pre_upgrade"] = info.ArchLinux.Scripts.PreUpgrade
   537  	}
   538  
   539  	if info.ArchLinux.Scripts.PostUpgrade != "" {
   540  		scripts["post_upgrade"] = info.ArchLinux.Scripts.PostUpgrade
   541  	}
   542  
   543  	if len(scripts) == 0 {
   544  		return nil
   545  	}
   546  
   547  	buf := &bytes.Buffer{}
   548  
   549  	err := writeScripts(buf, scripts)
   550  	if err != nil {
   551  		return err
   552  	}
   553  
   554  	err = tw.WriteHeader(&tar.Header{
   555  		Typeflag: tar.TypeReg,
   556  		Mode:     0o644,
   557  		Name:     ".INSTALL",
   558  		Size:     int64(buf.Len()),
   559  		ModTime:  modtime.Get(info.MTime),
   560  	})
   561  	if err != nil {
   562  		return err
   563  	}
   564  
   565  	_, err = io.Copy(tw, buf)
   566  	return err
   567  }
   568  
   569  func writeScripts(w io.Writer, scripts map[string]string) error {
   570  	for _, script := range maps.Keys(scripts) {
   571  		fmt.Fprintf(w, "function %s() {\n", script)
   572  
   573  		fl, err := os.Open(scripts[script])
   574  		if err != nil {
   575  			return err
   576  		}
   577  		defer fl.Close() //nolint: errcheck
   578  
   579  		_, err = io.Copy(w, fl)
   580  		if err != nil {
   581  			return err
   582  		}
   583  
   584  		_ = fl.Close()
   585  
   586  		_, err = io.WriteString(w, "\n}\n\n")
   587  		if err != nil {
   588  			return err
   589  		}
   590  	}
   591  
   592  	return nil
   593  }