1
1
mirror of https://github.com/goreleaser/nfpm synced 2025-04-11 20:23:38 +02:00
nfpm/ipk/ipk.go
Carlos Alexandro Becker fa30aa2d34
feat(rpm): support %config(missingok) (#854)
* feat(rpm): support %config(missingok)

closes #853

* Update files/files.go
2024-09-23 09:07:54 -03:00

513 lines
13 KiB
Go

// Package ipk implements nfpm.Packager providing .ipk bindings.
//
// IPK is a package format used by the opkg package manager, which is very
// similar to the Debian package format. Generally, the package format is
// stripped down and simplified compared to the Debian package format.
// Yocto/OpenEmbedded/OpenWRT uses the IPK format for its package management.
package ipk
import (
"archive/tar"
"bufio"
"bytes"
"fmt"
"io"
"strings"
"text/template"
"time"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/deprecation"
"github.com/goreleaser/nfpm/v2/files"
"github.com/goreleaser/nfpm/v2/internal/modtime"
)
const packagerName = "ipk"
// nolint: gochecknoinits
func init() {
nfpm.RegisterPackager(packagerName, Default)
}
// nolint: gochecknoglobals
var archToIPK = map[string]string{
// all --> all
"386": "i386",
"amd64": "x86_64",
"arm64": "arm64",
"arm5": "armel",
"arm6": "armhf",
"arm7": "armhf",
"mips64le": "mips64el",
"mipsle": "mipsel",
"ppc64le": "ppc64el",
"s390": "s390x",
}
func ensureValidArch(info *nfpm.Info) *nfpm.Info {
if info.IPK.Arch != "" {
info.Arch = info.IPK.Arch
} else if arch, ok := archToIPK[info.Arch]; ok {
info.Arch = arch
}
return info
}
// Default ipk packager.
// nolint: gochecknoglobals
var Default = &IPK{}
// IPK is a ipk packager implementation.
type IPK struct{}
// ConventionalFileName returns a file name according
// to the conventions for ipk packages. Ipk packages generally follow
// the conventions set by debian. See:
// https://manpages.debian.org/buster/dpkg-dev/dpkg-name.1.en.html
func (*IPK) ConventionalFileName(info *nfpm.Info) string {
info = ensureValidArch(info)
version := info.Version
if info.Prerelease != "" {
version += "~" + info.Prerelease
}
if info.VersionMetadata != "" {
version += "+" + info.VersionMetadata
}
if info.Release != "" {
version += "-" + info.Release
}
// package_version_architecture.package-type
return fmt.Sprintf("%s_%s_%s.ipk", info.Name, version, info.Arch)
}
// ConventionalExtension returns the file name conventionally used for IPK packages
func (*IPK) ConventionalExtension() string {
return ".ipk"
}
// SetPackagerDefaults sets the default values for the IPK packager.
func (*IPK) SetPackagerDefaults(info *nfpm.Info) {
// Priority should be set on all packages per:
// https://www.debian.org/doc/debian-policy/ch-archive.html#priorities
// "optional" seems to be the safe/sane default here
if info.Priority == "" {
info.Priority = "optional"
}
// The safe thing here feels like defaulting to something like below.
// That will prevent existing configs from breaking anyway... Wondering
// if in the long run we should be more strict about this and error when
// not set?
if strings.TrimSpace(info.Maintainer) == "" {
deprecation.Println("Leaving the 'maintainer' field unset will not be allowed in a future version")
info.Maintainer = "Unset Maintainer <unset@localhost>"
}
}
// Package writes a new ipk package to the given writer using the given info.
func (d *IPK) Package(info *nfpm.Info, ipk io.Writer) error {
info = ensureValidArch(info)
if err := nfpm.PrepareForPackager(info, packagerName); err != nil {
return err
}
// Set up some ipk specific defaults
d.SetPackagerDefaults(info)
// Strip out any custom fields that are disallowed.
stripDisallowedFields(info)
contents, err := newTGZ("ipk",
func(tw *tar.Writer) error {
return createIPK(info, tw)
},
)
if err != nil {
return err
}
_, err = ipk.Write(contents)
return err
}
// createIPK creates a new ipk package using the given tar writer and info.
func createIPK(info *nfpm.Info, ipk *tar.Writer) error {
var installSize int64
data, err := newTGZ("data.tar.gz",
func(tw *tar.Writer) error {
var err error
installSize, err = populateDataTar(info, tw)
return err
},
)
if err != nil {
return err
}
control, err := newTGZ("control.tar.gz",
func(tw *tar.Writer) error {
return populateControlTar(info, tw, installSize)
},
)
if err != nil {
return err
}
mtime := modtime.Get(info.MTime)
if err := writeToFile(ipk, "debian-binary", []byte("2.0\n"), mtime); err != nil {
return err
}
if err := writeToFile(ipk, "control.tar.gz", control, mtime); err != nil {
return err
}
if err := writeToFile(ipk, "data.tar.gz", data, mtime); err != nil {
return err
}
return nil
}
// populateDataTar populates the data tarball with the files specified in the info.
func populateDataTar(info *nfpm.Info, tw *tar.Writer) (instSize int64, err error) {
// create files and implicit directories
for _, file := range info.Contents {
var size int64
switch file.Type {
case files.TypeDir, files.TypeImplicitDir:
err = tw.WriteHeader(
&tar.Header{
Name: files.AsExplicitRelativePath(file.Destination),
Typeflag: tar.TypeDir,
Format: tar.FormatGNU,
ModTime: modtime.Get(info.MTime),
Mode: int64(file.FileInfo.Mode),
Uname: file.FileInfo.Owner,
Gname: file.FileInfo.Group,
})
case files.TypeSymlink:
err = tw.WriteHeader(
&tar.Header{
Name: files.AsExplicitRelativePath(file.Destination),
Typeflag: tar.TypeSymlink,
Format: tar.FormatGNU,
ModTime: modtime.Get(info.MTime),
Linkname: file.Source,
})
case files.TypeFile, files.TypeTree, files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK:
size, err = writeFile(tw, file)
default:
// ignore everything else
}
if err != nil {
return 0, err
}
instSize += size
}
return instSize, nil
}
// getScripts returns the scripts for the given info.
func getScripts(info *nfpm.Info, mtime time.Time) []files.Content {
return []files.Content{
{
Destination: "preinst",
Source: info.Scripts.PreInstall,
FileInfo: &files.ContentFileInfo{
Mode: 0o755,
MTime: mtime,
},
}, {
Destination: "postinst",
Source: info.Scripts.PostInstall,
FileInfo: &files.ContentFileInfo{
Mode: 0o755,
MTime: mtime,
},
}, {
Destination: "prerm",
Source: info.Scripts.PreRemove,
FileInfo: &files.ContentFileInfo{
Mode: 0o755,
MTime: mtime,
},
}, {
Destination: "postrm",
Source: info.Scripts.PostRemove,
FileInfo: &files.ContentFileInfo{
Mode: 0o755,
MTime: mtime,
},
},
}
}
// populateControlTar populates the control tarball with the control files defined
// in the info.
func populateControlTar(info *nfpm.Info, out *tar.Writer, instSize int64) error {
var body bytes.Buffer
cd := controlData{
Info: info,
InstalledSize: instSize / 1024,
}
if err := renderControl(&body, cd); err != nil {
return err
}
mtime := modtime.Get(info.MTime)
if err := writeToFile(out, "./control", body.Bytes(), mtime); err != nil {
return err
}
if err := writeToFile(out, "./conffiles", conffiles(info), mtime); err != nil {
return err
}
scripts := getScripts(info, mtime)
for _, file := range scripts {
if file.Source != "" {
if _, err := writeFile(out, &file); err != nil {
return err
}
}
}
return nil
}
// conffiles returns the conffiles file bytes for the given info.
func conffiles(info *nfpm.Info) []byte {
// nolint: prealloc
var confs []string
for _, file := range info.Contents {
switch file.Type {
case files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK:
confs = append(confs, files.NormalizeAbsoluteFilePath(file.Destination))
}
}
return []byte(strings.Join(confs, "\n") + "\n")
}
// The ipk format is not formally defined, but it is similar to the deb format.
// The two sources that were used to create this template are:
// - https://git.yoctoproject.org/opkg/
// - https://github.com/openwrt/opkg-lede
//
// Supported Fields
//
// R = Required
// O = Optional
// e = Extra
// - = Not Supported/Ignored/Extra
//
//
// OpenWRT Yocto
// | |
// | Field | W | Y | Status |
// |----------------|---|---|--------|
// | ABIVersion | O | - | ✓
// | Alternatives | O | - | ✓
// | Architecture | R | R | ✓
// | Auto-Installed | O | O | ✓
// | Conffiles | O | O | not needed since config files are listed in .conffiles
// | Conflicts | O | O | ✓
// | Depends | R | R | ✓
// | Description | R | R | ✓
// | Essential | O | O | ✓
// | Filename | - | - | an opkg field, not a package field
// | Homepage | e | e | ✓
// | Installed-Size | O | O | ✓
// | Installed-Time | - | - | an opkg field, not a package field
// | License | e | e | ✓
// | Maintainer | R | R | ✓
// | MD5sum | - | - | insecure, not supported
// | Package | R | R | ✓
// | Pre-Depends | e | O | ✓
// | Priority | R | R | ✓
// | Provides | O | O | ✓
// | Recommends | O | O | ✓
// | Replaces | O | O | ✓
// | Section | O | O | ✓
// | SHA256sum | - | - | an opkg field, not a package field
// | Size | - | - | an opkg field, not a package field
// | Source | - | - | use the Fields field
// | Status | - | - | an opkg state, not a package field
// | Suggests | O | O | ✓
// | Tags | O | O | ✓
// | Vendor | e | e | ✓
// | Version | R | R | ✓
//
// If any values in user supplied Fields are found to be duplicates of the above
// fields, they will be stripped out.
// nolint: gochecknoglobals
var controlFields = []string{
"ABIVersion",
"Alternatives",
"Architecture",
"Auto-Installed",
"Conffiles",
"Conflicts",
"Depends",
"Description",
"Essential",
"Filename",
"Homepage",
"Installed-Size",
"Installed-Time",
"License",
"Maintainer",
"MD5sum",
"Package",
"Pre-Depends",
"Priority",
"Provides",
"Recommends",
"Replaces",
"Section",
"SHA256sum",
"Size",
// "Source", Allowed
"Status",
"Suggests",
"Tags",
"Vendor",
"Version",
}
// stripDisallowedFields strips out any fields that are disallowed in the ipk
// format, ignoring case.
func stripDisallowedFields(info *nfpm.Info) {
for key := range info.IPK.Fields {
for _, disallowed := range controlFields {
if strings.EqualFold(key, disallowed) {
delete(info.IPK.Fields, key)
}
}
}
}
const controlTemplate = `
{{- /* Mandatory fields */ -}}
Architecture: {{.Info.Arch}}
Description: {{multiline .Info.Description}}
Maintainer: {{.Info.Maintainer}}
Package: {{.Info.Name}}
Priority: {{.Info.Priority}}
Version: {{ if .Info.Epoch}}{{ .Info.Epoch }}:{{ end }}{{.Info.Version}}
{{- if .Info.Prerelease}}~{{ .Info.Prerelease }}{{- end }}
{{- if .Info.VersionMetadata}}+{{ .Info.VersionMetadata }}{{- end }}
{{- if .Info.Release}}-{{ .Info.Release }}{{- end }}
{{- /* Optional fields */ -}}
{{- if .Info.IPK.ABIVersion}}
ABIVersion: {{.Info.IPK.ABIVersion}}
{{- end}}
{{- if .Info.IPK.Alternatives}}
Alternatives: {{ range $index, $element := .Info.IPK.Alternatives }}{{ if $index }}, {{end}}{{ $element.Priority }}:{{ $element.LinkName}}:{{ $element.Target}}{{- end }}
{{- end}}
{{- if .Info.IPK.AutoInstalled}}
Auto-Installed: yes
{{- end }}
{{- with .Info.Conflicts}}
Conflicts: {{join .}}
{{- end }}
{{- with .Info.Depends}}
Depends: {{join .}}
{{- end }}
{{- if .Info.IPK.Essential}}
Essential: yes
{{- end }}
{{- if .Info.Homepage}}
Homepage: {{.Info.Homepage}}
{{- end }}
{{- if .Info.License}}
License: {{.Info.License}}
{{- end }}
{{- if .InstalledSize }}
Installed-Size: {{.InstalledSize}}
{{- end }}
{{- with .Info.IPK.Predepends}}
Pre-Depends: {{join .}}
{{- end }}
{{- with nonEmpty .Info.Provides}}
Provides: {{join .}}
{{- end }}
{{- with .Info.Recommends}}
Recommends: {{join .}}
{{- end }}
{{- with .Info.Replaces}}
Replaces: {{join .}}
{{- end }}
{{- if .Info.Section}}
Section: {{.Info.Section}}
{{- end }}
{{- with .Info.Suggests}}
Suggests: {{join .}}
{{- end }}
{{- with .Info.IPK.Tags}}
Tags: {{join .}}
{{- end }}
{{- if .Info.Vendor}}
Vendor: {{.Info.Vendor}}
{{- end }}
{{- range $key, $value := .Info.IPK.Fields }}
{{- if $value }}
{{$key}}: {{$value}}
{{- end }}
{{- end }}
`
type controlData struct {
Info *nfpm.Info
InstalledSize int64
}
func renderControl(w io.Writer, data controlData) error {
tmpl := template.New("control")
tmpl.Funcs(template.FuncMap{
"join": func(strs []string) string {
return strings.Trim(strings.Join(strs, ", "), " ")
},
"multiline": func(strs string) string {
var b strings.Builder
s := bufio.NewScanner(strings.NewReader(strings.TrimSpace(strs)))
s.Scan()
b.Write(bytes.TrimSpace(s.Bytes()))
for s.Scan() {
b.WriteString("\n ")
l := bytes.TrimSpace(s.Bytes())
if len(l) == 0 {
b.WriteByte('.')
} else {
b.Write(l)
}
}
return b.String()
},
"nonEmpty": func(strs []string) []string {
var result []string
for _, s := range strs {
s := strings.TrimSpace(s)
if s == "" {
continue
}
result = append(result, s)
}
return result
},
})
return template.Must(tmpl.Parse(controlTemplate)).Execute(w, data)
}