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

     1  // Package ipk implements nfpm.Packager providing .ipk bindings.
     2  //
     3  // IPK is a package format used by the opkg package manager, which is very
     4  // similar to the Debian package format.  Generally, the package format is
     5  // stripped down and simplified compared to the Debian package format.
     6  // Yocto/OpenEmbedded/OpenWRT uses the IPK format for its package management.
     7  package ipk
     8  
     9  import (
    10  	"archive/tar"
    11  	"bufio"
    12  	"bytes"
    13  	"fmt"
    14  	"io"
    15  	"strings"
    16  	"text/template"
    17  	"time"
    18  
    19  	"github.com/goreleaser/nfpm/v2"
    20  	"github.com/goreleaser/nfpm/v2/deprecation"
    21  	"github.com/goreleaser/nfpm/v2/files"
    22  	"github.com/goreleaser/nfpm/v2/internal/modtime"
    23  )
    24  
    25  const packagerName = "ipk"
    26  
    27  // nolint: gochecknoinits
    28  func init() {
    29  	nfpm.RegisterPackager(packagerName, Default)
    30  }
    31  
    32  // nolint: gochecknoglobals
    33  var archToIPK = map[string]string{
    34  	// all --> all
    35  	"386":      "i386",
    36  	"amd64":    "x86_64",
    37  	"arm64":    "arm64",
    38  	"arm5":     "armel",
    39  	"arm6":     "armhf",
    40  	"arm7":     "armhf",
    41  	"mips64le": "mips64el",
    42  	"mipsle":   "mipsel",
    43  	"ppc64le":  "ppc64el",
    44  	"s390":     "s390x",
    45  }
    46  
    47  func ensureValidArch(info *nfpm.Info) *nfpm.Info {
    48  	if info.IPK.Arch != "" {
    49  		info.Arch = info.IPK.Arch
    50  	} else if arch, ok := archToIPK[info.Arch]; ok {
    51  		info.Arch = arch
    52  	}
    53  
    54  	return info
    55  }
    56  
    57  // Default ipk packager.
    58  // nolint: gochecknoglobals
    59  var Default = &IPK{}
    60  
    61  // IPK is a ipk packager implementation.
    62  type IPK struct{}
    63  
    64  // ConventionalFileName returns a file name according
    65  // to the conventions for ipk packages. Ipk packages generally follow
    66  // the conventions set by debian.  See:
    67  // https://manpages.debian.org/buster/dpkg-dev/dpkg-name.1.en.html
    68  func (*IPK) ConventionalFileName(info *nfpm.Info) string {
    69  	info = ensureValidArch(info)
    70  
    71  	version := info.Version
    72  	if info.Prerelease != "" {
    73  		version += "~" + info.Prerelease
    74  	}
    75  
    76  	if info.VersionMetadata != "" {
    77  		version += "+" + info.VersionMetadata
    78  	}
    79  
    80  	if info.Release != "" {
    81  		version += "-" + info.Release
    82  	}
    83  
    84  	// package_version_architecture.package-type
    85  	return fmt.Sprintf("%s_%s_%s.ipk", info.Name, version, info.Arch)
    86  }
    87  
    88  // ConventionalExtension returns the file name conventionally used for IPK packages
    89  func (*IPK) ConventionalExtension() string {
    90  	return ".ipk"
    91  }
    92  
    93  // SetPackagerDefaults sets the default values for the IPK packager.
    94  func (*IPK) SetPackagerDefaults(info *nfpm.Info) {
    95  	// Priority should be set on all packages per:
    96  	//   https://www.debian.org/doc/debian-policy/ch-archive.html#priorities
    97  	// "optional" seems to be the safe/sane default here
    98  	if info.Priority == "" {
    99  		info.Priority = "optional"
   100  	}
   101  
   102  	// The safe thing here feels like defaulting to something like below.
   103  	// That will prevent existing configs from breaking anyway...  Wondering
   104  	// if in the long run we should be more strict about this and error when
   105  	// not set?
   106  	if strings.TrimSpace(info.Maintainer) == "" {
   107  		deprecation.Println("Leaving the 'maintainer' field unset will not be allowed in a future version")
   108  		info.Maintainer = "Unset Maintainer <unset@localhost>"
   109  	}
   110  }
   111  
   112  // Package writes a new ipk package to the given writer using the given info.
   113  func (d *IPK) Package(info *nfpm.Info, ipk io.Writer) error {
   114  	info = ensureValidArch(info)
   115  
   116  	if err := nfpm.PrepareForPackager(info, packagerName); err != nil {
   117  		return err
   118  	}
   119  
   120  	// Set up some ipk specific defaults
   121  	d.SetPackagerDefaults(info)
   122  
   123  	// Strip out any custom fields that are disallowed.
   124  	stripDisallowedFields(info)
   125  
   126  	contents, err := newTGZ("ipk",
   127  		func(tw *tar.Writer) error {
   128  			return createIPK(info, tw)
   129  		},
   130  	)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	_, err = ipk.Write(contents)
   136  
   137  	return err
   138  }
   139  
   140  // createIPK creates a new ipk package using the given tar writer and info.
   141  func createIPK(info *nfpm.Info, ipk *tar.Writer) error {
   142  	var installSize int64
   143  
   144  	data, err := newTGZ("data.tar.gz",
   145  		func(tw *tar.Writer) error {
   146  			var err error
   147  			installSize, err = populateDataTar(info, tw)
   148  			return err
   149  		},
   150  	)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	control, err := newTGZ("control.tar.gz",
   156  		func(tw *tar.Writer) error {
   157  			return populateControlTar(info, tw, installSize)
   158  		},
   159  	)
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	mtime := modtime.Get(info.MTime)
   165  
   166  	if err := writeToFile(ipk, "debian-binary", []byte("2.0\n"), mtime); err != nil {
   167  		return err
   168  	}
   169  
   170  	if err := writeToFile(ipk, "control.tar.gz", control, mtime); err != nil {
   171  		return err
   172  	}
   173  
   174  	if err := writeToFile(ipk, "data.tar.gz", data, mtime); err != nil {
   175  		return err
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  // populateDataTar populates the data tarball with the files specified in the info.
   182  func populateDataTar(info *nfpm.Info, tw *tar.Writer) (instSize int64, err error) {
   183  	// create files and implicit directories
   184  	for _, file := range info.Contents {
   185  		var size int64
   186  
   187  		switch file.Type {
   188  		case files.TypeDir, files.TypeImplicitDir:
   189  			err = tw.WriteHeader(
   190  				&tar.Header{
   191  					Name:     files.AsExplicitRelativePath(file.Destination),
   192  					Typeflag: tar.TypeDir,
   193  					Format:   tar.FormatGNU,
   194  					ModTime:  modtime.Get(info.MTime),
   195  					Mode:     int64(file.FileInfo.Mode),
   196  					Uname:    file.FileInfo.Owner,
   197  					Gname:    file.FileInfo.Group,
   198  				})
   199  		case files.TypeSymlink:
   200  			err = tw.WriteHeader(
   201  				&tar.Header{
   202  					Name:     files.AsExplicitRelativePath(file.Destination),
   203  					Typeflag: tar.TypeSymlink,
   204  					Format:   tar.FormatGNU,
   205  					ModTime:  modtime.Get(info.MTime),
   206  					Linkname: file.Source,
   207  				})
   208  		case files.TypeFile, files.TypeTree, files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK:
   209  			size, err = writeFile(tw, file)
   210  		default:
   211  			// ignore everything else
   212  		}
   213  		if err != nil {
   214  			return 0, err
   215  		}
   216  		instSize += size
   217  	}
   218  
   219  	return instSize, nil
   220  }
   221  
   222  // getScripts returns the scripts for the given info.
   223  func getScripts(info *nfpm.Info, mtime time.Time) []files.Content {
   224  	return []files.Content{
   225  		{
   226  			Destination: "preinst",
   227  			Source:      info.Scripts.PreInstall,
   228  			FileInfo: &files.ContentFileInfo{
   229  				Mode:  0o755,
   230  				MTime: mtime,
   231  			},
   232  		}, {
   233  			Destination: "postinst",
   234  			Source:      info.Scripts.PostInstall,
   235  			FileInfo: &files.ContentFileInfo{
   236  				Mode:  0o755,
   237  				MTime: mtime,
   238  			},
   239  		}, {
   240  			Destination: "prerm",
   241  			Source:      info.Scripts.PreRemove,
   242  			FileInfo: &files.ContentFileInfo{
   243  				Mode:  0o755,
   244  				MTime: mtime,
   245  			},
   246  		}, {
   247  			Destination: "postrm",
   248  			Source:      info.Scripts.PostRemove,
   249  			FileInfo: &files.ContentFileInfo{
   250  				Mode:  0o755,
   251  				MTime: mtime,
   252  			},
   253  		},
   254  	}
   255  }
   256  
   257  // populateControlTar populates the control tarball with the control files defined
   258  // in the info.
   259  func populateControlTar(info *nfpm.Info, out *tar.Writer, instSize int64) error {
   260  	var body bytes.Buffer
   261  
   262  	cd := controlData{
   263  		Info:          info,
   264  		InstalledSize: instSize / 1024,
   265  	}
   266  
   267  	if err := renderControl(&body, cd); err != nil {
   268  		return err
   269  	}
   270  
   271  	mtime := modtime.Get(info.MTime)
   272  	if err := writeToFile(out, "./control", body.Bytes(), mtime); err != nil {
   273  		return err
   274  	}
   275  	if err := writeToFile(out, "./conffiles", conffiles(info), mtime); err != nil {
   276  		return err
   277  	}
   278  
   279  	scripts := getScripts(info, mtime)
   280  	for _, file := range scripts {
   281  		if file.Source != "" {
   282  			if _, err := writeFile(out, &file); err != nil {
   283  				return err
   284  			}
   285  		}
   286  	}
   287  	return nil
   288  }
   289  
   290  // conffiles returns the conffiles file bytes for the given info.
   291  func conffiles(info *nfpm.Info) []byte {
   292  	// nolint: prealloc
   293  	var confs []string
   294  	for _, file := range info.Contents {
   295  		switch file.Type {
   296  		case files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK:
   297  			confs = append(confs, files.NormalizeAbsoluteFilePath(file.Destination))
   298  		}
   299  	}
   300  	return []byte(strings.Join(confs, "\n") + "\n")
   301  }
   302  
   303  // The ipk format is not formally defined, but it is similar to the deb format.
   304  // The two sources that were used to create this template are:
   305  // - https://git.yoctoproject.org/opkg/
   306  // - https://github.com/openwrt/opkg-lede
   307  //
   308  // Supported Fields
   309  //
   310  // R = Required
   311  // O = Optional
   312  // e = Extra
   313  // - = Not Supported/Ignored/Extra
   314  //
   315  //
   316  //              OpenWRT   Yocto
   317  //                    |   |
   318  // | Field          | W | Y | Status |
   319  // |----------------|---|---|--------|
   320  // | ABIVersion     | O | - | ✓
   321  // | Alternatives   | O | - | ✓
   322  // | Architecture   | R | R | ✓
   323  // | Auto-Installed | O | O | ✓
   324  // | Conffiles      | O | O | not needed since config files are listed in .conffiles
   325  // | Conflicts      | O | O | ✓
   326  // | Depends        | R | R | ✓
   327  // | Description    | R | R | ✓
   328  // | Essential      | O | O | ✓
   329  // | Filename       | - | - | an opkg field, not a package field
   330  // | Homepage       | e | e | ✓
   331  // | Installed-Size | O | O | ✓
   332  // | Installed-Time | - | - | an opkg field, not a package field
   333  // | License        | e | e | ✓
   334  // | Maintainer     | R | R | ✓
   335  // | MD5sum		    | - | - | insecure, not supported
   336  // | Package        | R | R | ✓
   337  // | Pre-Depends    | e | O | ✓
   338  // | Priority       | R | R | ✓
   339  // | Provides       | O | O | ✓
   340  // | Recommends     | O | O | ✓
   341  // | Replaces       | O | O | ✓
   342  // | Section        | O | O | ✓
   343  // | SHA256sum      | - | - | an opkg field, not a package field
   344  // | Size           | - | - | an opkg field, not a package field
   345  // | Source         | - | - | use the Fields field
   346  // | Status		    | - | - | an opkg state, not a package field
   347  // | Suggests       | O | O | ✓
   348  // | Tags           | O | O | ✓
   349  // | Vendor         | e | e | ✓
   350  // | Version        | R | R | ✓
   351  //
   352  // If any values in user supplied Fields are found to be duplicates of the above
   353  // fields, they will be stripped out.
   354  
   355  // nolint: gochecknoglobals
   356  var controlFields = []string{
   357  	"ABIVersion",
   358  	"Alternatives",
   359  	"Architecture",
   360  	"Auto-Installed",
   361  	"Conffiles",
   362  	"Conflicts",
   363  	"Depends",
   364  	"Description",
   365  	"Essential",
   366  	"Filename",
   367  	"Homepage",
   368  	"Installed-Size",
   369  	"Installed-Time",
   370  	"License",
   371  	"Maintainer",
   372  	"MD5sum",
   373  	"Package",
   374  	"Pre-Depends",
   375  	"Priority",
   376  	"Provides",
   377  	"Recommends",
   378  	"Replaces",
   379  	"Section",
   380  	"SHA256sum",
   381  	"Size",
   382  	// "Source", Allowed
   383  	"Status",
   384  	"Suggests",
   385  	"Tags",
   386  	"Vendor",
   387  	"Version",
   388  }
   389  
   390  // stripDisallowedFields strips out any fields that are disallowed in the ipk
   391  // format, ignoring case.
   392  func stripDisallowedFields(info *nfpm.Info) {
   393  	for key := range info.IPK.Fields {
   394  		for _, disallowed := range controlFields {
   395  			if strings.EqualFold(key, disallowed) {
   396  				delete(info.IPK.Fields, key)
   397  			}
   398  		}
   399  	}
   400  }
   401  
   402  const controlTemplate = `
   403  {{- /* Mandatory fields */ -}}
   404  Architecture: {{.Info.Arch}}
   405  Description: {{multiline .Info.Description}}
   406  Maintainer: {{.Info.Maintainer}}
   407  Package: {{.Info.Name}}
   408  Priority: {{.Info.Priority}}
   409  Version: {{ if .Info.Epoch}}{{ .Info.Epoch }}:{{ end }}{{.Info.Version}}
   410           {{- if .Info.Prerelease}}~{{ .Info.Prerelease }}{{- end }}
   411           {{- if .Info.VersionMetadata}}+{{ .Info.VersionMetadata }}{{- end }}
   412           {{- if .Info.Release}}-{{ .Info.Release }}{{- end }}
   413  {{- /* Optional fields */ -}}
   414  {{- if .Info.IPK.ABIVersion}}
   415  ABIVersion: {{.Info.IPK.ABIVersion}}
   416  {{- end}}
   417  {{- if .Info.IPK.Alternatives}}
   418  Alternatives: {{ range $index, $element := .Info.IPK.Alternatives }}{{ if $index }}, {{end}}{{ $element.Priority }}:{{ $element.LinkName}}:{{ $element.Target}}{{- end }}
   419  {{- end}}
   420  {{- if .Info.IPK.AutoInstalled}}
   421  Auto-Installed: yes
   422  {{- end }}
   423  {{- with .Info.Conflicts}}
   424  Conflicts: {{join .}}
   425  {{- end }}
   426  {{- with .Info.Depends}}
   427  Depends: {{join .}}
   428  {{- end }}
   429  {{- if .Info.IPK.Essential}}
   430  Essential: yes
   431  {{- end }}
   432  {{- if .Info.Homepage}}
   433  Homepage: {{.Info.Homepage}}
   434  {{- end }}
   435  {{- if .Info.License}}
   436  License: {{.Info.License}}
   437  {{- end }}
   438  {{- if .InstalledSize }}
   439  Installed-Size: {{.InstalledSize}}
   440  {{- end }}
   441  {{- with .Info.IPK.Predepends}}
   442  Pre-Depends: {{join .}}
   443  {{- end }}
   444  {{- with nonEmpty .Info.Provides}}
   445  Provides: {{join .}}
   446  {{- end }}
   447  {{- with .Info.Recommends}}
   448  Recommends: {{join .}}
   449  {{- end }}
   450  {{- with .Info.Replaces}}
   451  Replaces: {{join .}}
   452  {{- end }}
   453  {{- if .Info.Section}}
   454  Section: {{.Info.Section}}
   455  {{- end }}
   456  {{- with .Info.Suggests}}
   457  Suggests: {{join .}}
   458  {{- end }}
   459  {{- with .Info.IPK.Tags}}
   460  Tags: {{join .}}
   461  {{- end }}
   462  {{- if .Info.Vendor}}
   463  Vendor: {{.Info.Vendor}}
   464  {{- end }}
   465  {{- range $key, $value := .Info.IPK.Fields }}
   466  {{- if $value }}
   467  {{$key}}: {{$value}}
   468  {{- end }}
   469  {{- end }}
   470  `
   471  
   472  type controlData struct {
   473  	Info          *nfpm.Info
   474  	InstalledSize int64
   475  }
   476  
   477  func renderControl(w io.Writer, data controlData) error {
   478  	tmpl := template.New("control")
   479  	tmpl.Funcs(template.FuncMap{
   480  		"join": func(strs []string) string {
   481  			return strings.Trim(strings.Join(strs, ", "), " ")
   482  		},
   483  		"multiline": func(strs string) string {
   484  			var b strings.Builder
   485  			s := bufio.NewScanner(strings.NewReader(strings.TrimSpace(strs)))
   486  			s.Scan()
   487  			b.Write(bytes.TrimSpace(s.Bytes()))
   488  			for s.Scan() {
   489  				b.WriteString("\n ")
   490  				l := bytes.TrimSpace(s.Bytes())
   491  				if len(l) == 0 {
   492  					b.WriteByte('.')
   493  				} else {
   494  					b.Write(l)
   495  				}
   496  			}
   497  			return b.String()
   498  		},
   499  		"nonEmpty": func(strs []string) []string {
   500  			var result []string
   501  			for _, s := range strs {
   502  				s := strings.TrimSpace(s)
   503  				if s == "" {
   504  					continue
   505  				}
   506  				result = append(result, s)
   507  			}
   508  			return result
   509  		},
   510  	})
   511  	return template.Must(tmpl.Parse(controlTemplate)).Execute(w, data)
   512  }