1
1
Fork 0
mirror of https://github.com/goreleaser/nfpm synced 2024-05-19 00:56:13 +02:00

feat: support for ipk packages

Implements #507.

* Adds ipk support for keywords used by OpenWRT and Yocto.
* MD5sum is explicitly excluded due to insecurity.
* SHA256Sum excluded due packages not being individually signed,
  instead, the feed of packages is checksummed and signed externally.
* Adds code to nfpm package to automatically enumerate the supported
  packaging types where possible.
This commit is contained in:
schmidtw 2024-04-29 21:07:01 -07:00
parent 409b51628a
commit 2210695091
No known key found for this signature in database
GPG Key ID: 0A9BAC78460E15F4
28 changed files with 2237 additions and 7 deletions

View File

@ -65,7 +65,7 @@ jobs:
acceptance-tests:
strategy:
matrix:
pkgFormat: [deb, rpm, apk, archlinux]
pkgFormat: [deb, rpm, apk, archlinux, ipk]
pkgPlatform: [amd64, arm64, 386, ppc64le, armv6, armv7, s390x]
runs-on: ubuntu-latest
env:

View File

@ -15,6 +15,7 @@ import (
_ "github.com/goreleaser/nfpm/v2/apk"
_ "github.com/goreleaser/nfpm/v2/arch"
_ "github.com/goreleaser/nfpm/v2/deb"
_ "github.com/goreleaser/nfpm/v2/ipk"
_ "github.com/goreleaser/nfpm/v2/rpm"
"github.com/stretchr/testify/require"
)
@ -23,6 +24,7 @@ import (
var formatArchs = map[string][]string{
"apk": {"amd64", "arm64", "386", "ppc64le", "armv6", "armv7", "s390x"},
"deb": {"amd64", "arm64", "ppc64le", "armv7", "s390x"},
"ipk": {"x86_64", "aarch64_generic"},
"rpm": {"amd64", "arm64", "ppc64le"},
"archlinux": {"amd64"},
}
@ -261,6 +263,38 @@ func TestDebSpecific(t *testing.T) {
}
}
func TestIPKSpecific(t *testing.T) {
t.Parallel()
format := "ipk"
testNames := []string{
"alternatives",
"conflicts",
"predepends",
}
for _, name := range testNames {
for _, arch := range formatArchs[format] {
func(t *testing.T, testName, testArch string) {
t.Run(fmt.Sprintf("%s/%s/%s", format, testArch, testName), func(t *testing.T) {
t.Parallel()
if testArch == "ppc64le" && os.Getenv("NO_TEST_PPC64LE") == "true" {
t.Skip("ppc64le arch not supported in pipeline")
}
accept(t, acceptParms{
Name: fmt.Sprintf("%s_%s", testName, testArch),
Conf: fmt.Sprintf("%s.%s.yaml", format, testName),
Format: format,
Docker: dockerParams{
File: fmt.Sprintf("%s.dockerfile", format),
Target: testName,
Arch: testArch,
},
})
})
}(t, name, arch)
}
}
}
func TestRPMSign(t *testing.T) {
t.Parallel()
for _, os := range []string{"centos9", "centos8", "fedora34", "fedora36", "fedora38"} {

View File

@ -33,7 +33,7 @@ func main() {
func buildVersion(version, commit, date, builtBy, treeState string) goversion.Info {
return goversion.GetVersionInfo(
goversion.WithAppDetails("nfpm", "a simple and 0-dependencies deb, rpm, apk and arch linux packager written in Go", website),
goversion.WithAppDetails("nfpm", "a simple and 0-dependencies apk, arch linux, deb, ipk, and rpm packager written in Go", website),
goversion.WithASCIIName(asciiArt),
func(i *goversion.Info) {
if commit != "" {

View File

@ -6,6 +6,7 @@ import (
"os"
"path"
"path/filepath"
"strings"
"github.com/goreleaser/nfpm/v2"
"github.com/spf13/cobra"
@ -37,9 +38,12 @@ func newPackageCmd() *packageCmd {
_ = cmd.MarkFlagFilename("config", "yaml", "yml")
cmd.Flags().StringVarP(&root.target, "target", "t", "", "where to save the generated package (filename, folder or empty for current folder)")
_ = cmd.MarkFlagFilename("target")
cmd.Flags().StringVarP(&root.packager, "packager", "p", "", "which packager implementation to use [apk|deb|rpm|archlinux]")
_ = cmd.RegisterFlagCompletionFunc("packager", cobra.FixedCompletions(
[]string{"apk", "deb", "rpm", "archlinux"},
pkgs := nfpm.Enumerate()
cmd.Flags().StringVarP(&root.packager, "packager", "p", "",
fmt.Sprintf("which packager implementation to use [%s]", strings.Join(pkgs, "|")))
_ = cmd.RegisterFlagCompletionFunc("packager", cobra.FixedCompletions(pkgs,
cobra.ShellCompDirectiveNoFileComp,
))

View File

@ -7,6 +7,7 @@ import (
_ "github.com/goreleaser/nfpm/v2/apk" // apk packager
_ "github.com/goreleaser/nfpm/v2/arch" // archlinux packager
_ "github.com/goreleaser/nfpm/v2/deb" // deb packager
_ "github.com/goreleaser/nfpm/v2/ipk" // ipk packager
_ "github.com/goreleaser/nfpm/v2/rpm" // rpm packager
"github.com/spf13/cobra"
)
@ -35,8 +36,8 @@ func newRootCmd(version goversion.Info, exit func(int)) *rootCmd {
}
cmd := &cobra.Command{
Use: "nfpm",
Short: "Packages apps on RPM, Deb, APK and Arch Linux formats based on a YAML configuration file",
Long: `nFPM is a simple and 0-dependencies deb, rpm, apk and arch linux packager written in Go.`,
Short: "Packages apps on RPM, Deb, APK, Arch Linux, and ipk formats based on a YAML configuration file",
Long: `nFPM is a simple and 0-dependencies apk, arch, deb, ipk and rpm linux packager written in Go.`,
Version: version.String(),
SilenceUsage: true,
SilenceErrors: true,

512
ipk/ipk.go Normal file
View File

@ -0,0 +1,512 @@
// 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:
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:
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)
}

1035
ipk/ipk_test.go Normal file
View File

@ -0,0 +1,1035 @@
package ipk
import (
"archive/tar"
"bytes"
"errors"
"flag"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"testing"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// nolint: gochecknoglobals
var update = flag.Bool("update", false, "update .golden files")
func exampleInfo() *nfpm.Info {
return nfpm.WithDefaults(&nfpm.Info{
Name: "foo",
Arch: "amd64",
Description: "Foo does things",
Priority: "extra",
Maintainer: "Carlos A Becker <pkg@carlosbecker.com>",
Version: "v1.0.0",
Section: "default",
Homepage: "http://carlosbecker.com",
Vendor: "nope",
Overridables: nfpm.Overridables{
Depends: []string{
"bash",
},
Recommends: []string{
"git",
},
Suggests: []string{
"bash",
},
Replaces: []string{
"svn",
},
Provides: []string{
"bzr",
},
Conflicts: []string{
"zsh",
},
Contents: []*files.Content{
{
Source: "../testdata/fake",
Destination: "/usr/bin/fake",
},
{
Source: "../testdata/whatever.conf",
Destination: "/usr/share/doc/fake/fake.txt",
},
{
Source: "../testdata/whatever.conf",
Destination: "/etc/fake/fake.conf",
Type: files.TypeConfig,
},
{
Source: "../testdata/whatever.conf",
Destination: "/etc/fake/fake2.conf",
Type: files.TypeConfigNoReplace,
},
{
Destination: "/var/log/whatever",
Type: files.TypeDir,
},
{
Destination: "/usr/share/whatever",
Type: files.TypeDir,
},
},
IPK: nfpm.IPK{
Predepends: []string{"less"},
},
},
})
}
func TestConventionalExtension(t *testing.T) {
require.Equal(t, ".ipk", Default.ConventionalExtension())
}
func TestIPK(t *testing.T) {
for _, arch := range []string{"386", "amd64"} {
arch := arch
t.Run(arch, func(t *testing.T) {
info := exampleInfo()
info.Arch = arch
err := Default.Package(info, io.Discard)
require.NoError(t, err)
})
}
}
func TestIPKPlatform(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "test*.deb")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, f.Close()) })
info := exampleInfo()
info.Platform = "darwin"
err = Default.Package(info, f)
require.NoError(t, err)
}
func extractIPKArchitecture(deb *bytes.Buffer) string {
for _, s := range strings.Split(deb.String(), "\n") {
if strings.Contains(s, "Architecture: ") {
return strings.TrimPrefix(s, "Architecture: ")
}
}
return ""
}
func splitIPKArchitecture(deb *bytes.Buffer) (string, string) {
a := extractIPKArchitecture(deb)
if strings.Contains(a, "-") {
f := strings.Split(a, "-")
return f[0], f[1]
}
return "linux", a
}
func TestIPKOS(t *testing.T) {
info := exampleInfo()
var buf bytes.Buffer
err := renderControl(&buf, controlData{info, 0})
require.NoError(t, err)
o, _ := splitIPKArchitecture(&buf)
require.Equal(t, "linux", o)
}
func TestIPKArch(t *testing.T) {
info := exampleInfo()
var buf bytes.Buffer
err := renderControl(&buf, controlData{info, 0})
require.NoError(t, err)
_, a := splitIPKArchitecture(&buf)
require.Equal(t, "amd64", a)
}
func extractIPKVersion(deb *bytes.Buffer) string {
for _, s := range strings.Split(deb.String(), "\n") {
if strings.Contains(s, "Version: ") {
return strings.TrimPrefix(s, "Version: ")
}
}
return ""
}
func TestIPKVersionWithDash(t *testing.T) {
info := exampleInfo()
info.Version = "1.0.0-beta"
err := Default.Package(info, io.Discard)
require.NoError(t, err)
}
func TestIPKVersion(t *testing.T) {
info := exampleInfo()
info.Version = "1.0.0" //nolint:golint,goconst
var buf bytes.Buffer
err := renderControl(&buf, controlData{info, 0})
require.NoError(t, err)
v := extractIPKVersion(&buf)
require.Equal(t, "1.0.0", v)
}
func TestIPKVersionWithRelease(t *testing.T) {
info := exampleInfo()
info.Version = "1.0.0" //nolint:golint,goconst
info.Release = "1"
var buf bytes.Buffer
err := renderControl(&buf, controlData{info, 0})
require.NoError(t, err)
v := extractIPKVersion(&buf)
require.Equal(t, "1.0.0-1", v)
}
func TestIPKVersionWithPrerelease(t *testing.T) {
var buf bytes.Buffer
info := exampleInfo()
info.Version = "1.0.0" //nolint:golint,goconst
info.Prerelease = "1"
err := renderControl(&buf, controlData{info, 0})
require.NoError(t, err)
v := extractIPKVersion(&buf)
require.Equal(t, "1.0.0~1", v)
}
func TestIPKVersionWithReleaseAndPrerelease(t *testing.T) {
var buf bytes.Buffer
info := exampleInfo()
info.Version = "1.0.0" //nolint:golint,goconst
info.Release = "2"
info.Prerelease = "rc1" //nolint:golint,goconst
err := renderControl(&buf, controlData{info, 0})
require.NoError(t, err)
v := extractIPKVersion(&buf)
require.Equal(t, "1.0.0~rc1-2", v)
}
func TestIPKVersionWithVersionMetadata(t *testing.T) {
var buf bytes.Buffer
info := exampleInfo()
info.Version = "1.0.0+meta" //nolint:golint,goconst
info.VersionMetadata = ""
err := renderControl(&buf, controlData{info, 0})
require.NoError(t, err)
v := extractIPKVersion(&buf)
require.Equal(t, "1.0.0+meta", v)
buf.Reset()
info.Version = "1.0.0" //nolint:golint,goconst
info.VersionMetadata = "meta"
err = renderControl(&buf, controlData{info, 0})
require.NoError(t, err)
v = extractIPKVersion(&buf)
require.Equal(t, "1.0.0+meta", v)
buf.Reset()
info.Version = "1.0.0+foo" //nolint:golint,goconst
info.Prerelease = "alpha"
info.VersionMetadata = "meta"
err = renderControl(&buf, controlData{nfpm.WithDefaults(info), 0})
require.NoError(t, err)
v = extractIPKVersion(&buf)
require.Equal(t, "1.0.0~alpha+meta", v)
}
func TestControl(t *testing.T) {
var w bytes.Buffer
require.NoError(t, renderControl(&w, controlData{
Info: exampleInfo(),
InstalledSize: 10,
}))
golden := "testdata/control.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func TestNoJoinsControl(t *testing.T) {
var w bytes.Buffer
require.NoError(t, renderControl(&w, controlData{
Info: nfpm.WithDefaults(&nfpm.Info{
Name: "foo",
Arch: "amd64",
Description: "Foo does things",
Priority: "extra",
Maintainer: "Carlos A Becker <pkg@carlosbecker.com>",
Version: "v1.0.0",
Section: "default",
Homepage: "http://carlosbecker.com",
Vendor: "nope",
Overridables: nfpm.Overridables{
Depends: []string{},
Recommends: []string{},
Suggests: []string{},
Replaces: []string{},
Provides: []string{},
Conflicts: []string{},
Contents: []*files.Content{},
},
}),
InstalledSize: 10,
}))
golden := "testdata/control2.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func TestVersionControl(t *testing.T) {
var w bytes.Buffer
require.NoError(t, renderControl(&w, controlData{
Info: nfpm.WithDefaults(&nfpm.Info{
Name: "foo",
Arch: "amd64",
Description: "Foo does things",
Priority: "extra",
Maintainer: "Carlos A Becker <pkg@carlosbecker.com>",
Version: "v1.0.0-beta+meta",
Release: "2",
Section: "default",
Homepage: "http://carlosbecker.com",
Vendor: "nope",
Overridables: nfpm.Overridables{
Depends: []string{},
Recommends: []string{},
Suggests: []string{},
Replaces: []string{},
Provides: []string{},
Conflicts: []string{},
Contents: []*files.Content{},
},
}),
InstalledSize: 10,
}))
golden := "testdata/control4.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func TestIPKFileDoesNotExist(t *testing.T) {
abs, err := filepath.Abs("../testdata/whatever.confzzz")
require.NoError(t, err)
err = Default.Package(
nfpm.WithDefaults(&nfpm.Info{
Name: "foo",
Arch: "amd64",
Description: "Foo does things",
Priority: "extra",
Maintainer: "Carlos A Becker <pkg@carlosbecker.com>",
Version: "1.0.0",
Section: "default",
Homepage: "http://carlosbecker.com",
Vendor: "nope",
Overridables: nfpm.Overridables{
Depends: []string{
"bash",
},
Contents: []*files.Content{
{
Source: "../testdata/fake",
Destination: "/usr/bin/fake",
},
{
Source: "../testdata/whatever.confzzz",
Destination: "/etc/fake/fake.conf",
Type: files.TypeConfig,
},
},
},
}),
io.Discard,
)
require.EqualError(t, err, fmt.Sprintf("matching \"%s\": file does not exist", filepath.ToSlash(abs)))
}
func TestIPKNoFiles(t *testing.T) {
err := Default.Package(
nfpm.WithDefaults(&nfpm.Info{
Name: "foo",
Arch: "amd64",
Description: "Foo does things",
Priority: "extra",
Maintainer: "Carlos A Becker <pkg@carlosbecker.com>",
Version: "1.0.0",
Section: "default",
Homepage: "http://carlosbecker.com",
Vendor: "nope",
Overridables: nfpm.Overridables{
Depends: []string{
"bash",
},
},
}),
io.Discard,
)
require.NoError(t, err)
}
func TestIPKNoInfo(t *testing.T) {
err := Default.Package(nfpm.WithDefaults(&nfpm.Info{}), io.Discard)
require.Error(t, err)
}
func TestConffiles(t *testing.T) {
info := nfpm.WithDefaults(&nfpm.Info{
Name: "minimal",
Arch: "arm64",
Description: "Minimal does nothing",
Priority: "extra",
Version: "1.0.0",
Section: "default",
Maintainer: "maintainer",
Overridables: nfpm.Overridables{
Contents: []*files.Content{
{
Source: "../testdata/fake",
Destination: "/etc/fake",
Type: files.TypeConfig,
},
},
},
})
err := nfpm.PrepareForPackager(info, packagerName)
require.NoError(t, err)
out := conffiles(info)
require.Equal(t, "/etc/fake\n", string(out), "should have a trailing empty line")
}
func TestMinimalFields(t *testing.T) {
var w bytes.Buffer
require.NoError(t, renderControl(&w, controlData{
Info: nfpm.WithDefaults(&nfpm.Info{
Name: "minimal",
Arch: "arm64",
Description: "Minimal does nothing",
Priority: "extra",
Version: "1.0.0",
Maintainer: "maintainer",
}),
}))
golden := "testdata/minimal.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func TestIPKEpoch(t *testing.T) {
var w bytes.Buffer
require.NoError(t, renderControl(&w, controlData{
Info: nfpm.WithDefaults(&nfpm.Info{
Name: "withepoch",
Arch: "arm64",
Description: "Has an epoch added to it's version",
Priority: "extra",
Epoch: "2",
Version: "1.0.0",
Section: "default",
}),
}))
golden := "testdata/withepoch.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func TestMultilineFields(t *testing.T) {
var w bytes.Buffer
require.NoError(t, renderControl(&w, controlData{
Info: nfpm.WithDefaults(&nfpm.Info{
Name: "multiline",
Arch: "riscv64",
Description: "This field is a\nmultiline field\n\nthat should work.",
Priority: "extra",
Version: "1.0.0",
Maintainer: "someone",
}),
}))
golden := "testdata/multiline.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func TestIPKConventionalFileName(t *testing.T) {
info := &nfpm.Info{
Name: "testpkg",
Arch: "all",
Maintainer: "maintainer",
}
testCases := []struct {
Version string
Release string
Prerelease string
Expected string
Metadata string
}{
{
Version: "1.2.3", Release: "", Prerelease: "", Metadata: "",
Expected: fmt.Sprintf("%s_1.2.3_%s.ipk", info.Name, info.Arch),
},
{
Version: "1.2.3", Release: "4", Prerelease: "", Metadata: "",
Expected: fmt.Sprintf("%s_1.2.3-4_%s.ipk", info.Name, info.Arch),
},
{
Version: "1.2.3", Release: "4", Prerelease: "5", Metadata: "",
Expected: fmt.Sprintf("%s_1.2.3~5-4_%s.ipk", info.Name, info.Arch),
},
{
Version: "1.2.3", Release: "", Prerelease: "5", Metadata: "",
Expected: fmt.Sprintf("%s_1.2.3~5_%s.ipk", info.Name, info.Arch),
},
{
Version: "1.2.3", Release: "1", Prerelease: "5", Metadata: "git",
Expected: fmt.Sprintf("%s_1.2.3~5+git-1_%s.ipk", info.Name, info.Arch),
},
}
for _, testCase := range testCases {
info.Version = testCase.Version
info.Release = testCase.Release
info.Prerelease = testCase.Prerelease
info.VersionMetadata = testCase.Metadata
require.Equal(t, testCase.Expected, Default.ConventionalFileName(info))
}
}
func TestSymlink(t *testing.T) {
var (
configFilePath = "/usr/share/doc/fake/fake.txt"
symlink = "/path/to/symlink"
symlinkTarget = configFilePath
)
info := &nfpm.Info{
Name: "symlink-in-files",
Arch: "amd64",
Description: "This package's config references a file via symlink.",
Version: "1.0.0",
Maintainer: "maintainer",
Overridables: nfpm.Overridables{
Contents: []*files.Content{
{
Source: "../testdata/whatever.conf",
Destination: configFilePath,
},
{
Source: symlinkTarget,
Destination: symlink,
Type: files.TypeSymlink,
},
},
},
}
err := nfpm.PrepareForPackager(info, packagerName)
require.NoError(t, err)
var buf bytes.Buffer
tarball := tar.NewWriter(&buf)
_, err = populateDataTar(info, tarball)
require.NoError(t, err)
require.NoError(t, tarball.Close())
packagedSymlinkHeader := extractFileHeaderFromTar(t, buf.Bytes(), symlink)
require.Equal(t, symlink, path.Join("/", packagedSymlinkHeader.Name)) // nolint:gosec
require.Equal(t, uint8(tar.TypeSymlink), packagedSymlinkHeader.Typeflag)
require.Equal(t, symlinkTarget, packagedSymlinkHeader.Linkname)
}
func TestEnsureRelativePrefixInTarballs(t *testing.T) {
info := exampleInfo()
info.Contents = []*files.Content{
{
Source: "/symlink/to/fake.txt",
Destination: "/usr/share/doc/fake/fake.txt",
Type: files.TypeSymlink,
},
}
info.Changelog = "../testdata/changelog.yaml"
err := nfpm.PrepareForPackager(info, packagerName)
require.NoError(t, err)
var dataBuf bytes.Buffer
dataTarball := tar.NewWriter(&dataBuf)
instSize, err := populateDataTar(info, dataTarball)
require.NoError(t, err)
require.NoError(t, dataTarball.Close())
testRelativePathPrefixInTar(t, dataBuf.Bytes())
var controlBuf bytes.Buffer
controlTarball := tar.NewWriter(&controlBuf)
err = populateControlTar(info, controlTarball, instSize)
require.NoError(t, err)
require.NoError(t, controlTarball.Close())
testRelativePathPrefixInTar(t, controlBuf.Bytes())
}
func TestDirectories(t *testing.T) {
info := exampleInfo()
info.Contents = []*files.Content{
{
Source: "../testdata/whatever.conf",
Destination: "/etc/foo/file",
},
{
Source: "../testdata/whatever.conf",
Destination: "/etc/bar/file",
},
{
Destination: "/etc/bar",
Type: files.TypeDir,
FileInfo: &files.ContentFileInfo{
Owner: "test",
Mode: 0o700,
},
},
{
Destination: "/etc/baz",
Type: files.TypeDir,
},
{
Destination: "/usr/lib/something/somethingelse",
Type: files.TypeDir,
},
}
require.NoError(t, nfpm.PrepareForPackager(info, packagerName))
var dataBuf bytes.Buffer
tarball := tar.NewWriter(&dataBuf)
_, err := populateDataTar(info, tarball)
require.NoError(t, err)
require.NoError(t, tarball.Close())
dataTarball := dataBuf.Bytes()
require.Equal(t, []string{
"./etc/",
"./etc/bar/",
"./etc/bar/file",
"./etc/baz/",
"./etc/foo/",
"./etc/foo/file",
"./usr/",
"./usr/lib/",
"./usr/lib/something/",
"./usr/lib/something/somethingelse/",
}, getTree(t, dataBuf.Bytes()))
// for ipk all implicit or explicit directories are created in the tarball
h := extractFileHeaderFromTar(t, dataTarball, "/etc")
require.Equal(t, h.Typeflag, byte(tar.TypeDir))
h = extractFileHeaderFromTar(t, dataTarball, "/etc/foo")
require.Equal(t, h.Typeflag, byte(tar.TypeDir))
h = extractFileHeaderFromTar(t, dataTarball, "/etc/bar")
require.Equal(t, h.Typeflag, byte(tar.TypeDir))
require.Equal(t, int64(0o700), h.Mode)
require.Equal(t, "test", h.Uname)
h = extractFileHeaderFromTar(t, dataTarball, "/etc/baz")
require.Equal(t, h.Typeflag, byte(tar.TypeDir))
h = extractFileHeaderFromTar(t, dataTarball, "/usr")
require.Equal(t, h.Typeflag, byte(tar.TypeDir))
h = extractFileHeaderFromTar(t, dataTarball, "/usr/lib")
require.Equal(t, h.Typeflag, byte(tar.TypeDir))
h = extractFileHeaderFromTar(t, dataTarball, "/usr/lib/something")
require.Equal(t, h.Typeflag, byte(tar.TypeDir))
h = extractFileHeaderFromTar(t, dataTarball, "/usr/lib/something/somethingelse")
require.Equal(t, h.Typeflag, byte(tar.TypeDir))
}
func TestNoDuplicateContents(t *testing.T) {
info := exampleInfo()
info.Contents = []*files.Content{
{
Source: "../testdata/whatever.conf",
Destination: "/etc/foo/file",
},
{
Source: "../testdata/whatever.conf",
Destination: "/etc/foo/file2",
},
{
Destination: "/etc/foo",
Type: files.TypeDir,
},
{
Destination: "/etc/baz",
Type: files.TypeDir,
},
}
require.NoError(t, nfpm.PrepareForPackager(info, packagerName))
var dataBuf bytes.Buffer
tarball := tar.NewWriter(&dataBuf)
_, err := populateDataTar(info, tarball)
require.NoError(t, err)
require.NoError(t, tarball.Close())
exists := map[string]bool{}
tr := tar.NewReader(bytes.NewReader(dataBuf.Bytes()))
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break // End of archive
}
require.NoError(t, err)
_, ok := exists[hdr.Name]
if ok {
t.Fatalf("%s exists more than once in tarball", hdr.Name)
}
exists[hdr.Name] = true
}
}
func testRelativePathPrefixInTar(tb testing.TB, tarFile []byte) {
tb.Helper()
tr := tar.NewReader(bytes.NewReader(tarFile))
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break // End of archive
}
require.NoError(tb, err)
require.True(tb, strings.HasPrefix(hdr.Name, "./"), "%s does not start with './'", hdr.Name)
}
}
func TestDisableGlobbing(t *testing.T) {
info := exampleInfo()
info.DisableGlobbing = true
info.Contents = []*files.Content{
{
Source: "../testdata/{file}[",
Destination: "/test/{file}[",
},
}
require.NoError(t, nfpm.PrepareForPackager(info, packagerName))
var dataBuf bytes.Buffer
tarball := tar.NewWriter(&dataBuf)
_, err := populateDataTar(info, tarball)
require.NoError(t, err)
require.NoError(t, tarball.Close())
expectedContent, err := os.ReadFile("../testdata/{file}[")
require.NoError(t, err)
actualContent := extractFileFromTar(t, dataBuf.Bytes(), "/test/{file}[")
require.Equal(t, expectedContent, actualContent)
}
func TestNoDuplicateAutocreatedDirectories(t *testing.T) {
info := exampleInfo()
info.DisableGlobbing = true
info.Contents = []*files.Content{
{
Source: "../testdata/fake",
Destination: "/etc/foo/bar",
},
{
Type: files.TypeDir,
Destination: "/etc/foo",
},
}
require.NoError(t, nfpm.PrepareForPackager(info, packagerName))
expected := map[string]bool{
"./etc/": true,
"./etc/foo/": true,
"./etc/foo/bar": true,
}
var dataBuf bytes.Buffer
tarball := tar.NewWriter(&dataBuf)
_, err := populateDataTar(info, tarball)
require.NoError(t, err)
require.NoError(t, tarball.Close())
contents := tarContents(t, dataBuf.Bytes())
if len(expected) != len(contents) {
t.Fatalf("contents has %d entries instead of %d: %#v", len(contents), len(expected), contents)
}
for _, entry := range contents {
if !expected[entry] {
t.Fatalf("unexpected content: %q", entry)
}
}
}
func TestNoDuplicateDirectories(t *testing.T) {
info := exampleInfo()
info.DisableGlobbing = true
info.Contents = []*files.Content{
{
Type: files.TypeDir,
Destination: "/etc/foo",
},
{
Type: files.TypeDir,
Destination: "/etc/foo/",
},
}
require.Error(t, nfpm.PrepareForPackager(info, packagerName))
}
func TestIgnoreUnrelatedFiles(t *testing.T) {
info := exampleInfo()
info.Contents = files.Contents{
{
Source: "../testdata/fake",
Destination: "/usr/bin/fake",
Packager: "rpm",
},
{
Source: "../testdata/whatever.conf",
Destination: "/usr/share/doc/fake/fake.txt",
Type: files.TypeRPMLicence,
},
{
Source: "../testdata/whatever.conf",
Destination: "/etc/fake/fake.conf",
Type: files.TypeRPMLicense,
},
{
Source: "../testdata/whatever.conf",
Destination: "/etc/fake/fake2.conf",
Type: files.TypeRPMReadme,
},
{
Destination: "/var/log/whatever",
Type: files.TypeRPMDoc,
},
}
require.NoError(t, nfpm.PrepareForPackager(info, packagerName))
var dataBuf bytes.Buffer
tarball := tar.NewWriter(&dataBuf)
_, err := populateDataTar(info, tarball)
require.NoError(t, err)
require.NoError(t, tarball.Close())
contents := tarContents(t, dataBuf.Bytes())
require.Empty(t, contents)
}
func TestEmptyButRequiredIPKFields(t *testing.T) {
item := nfpm.WithDefaults(&nfpm.Info{
Name: "foo",
Version: "v1.0.0",
})
Default.SetPackagerDefaults(item)
require.Equal(t, "optional", item.Priority)
require.Equal(t, "Unset Maintainer <unset@localhost>", item.Maintainer)
var deb bytes.Buffer
err := Default.Package(item, &deb)
require.NoError(t, err)
}
func TestArches(t *testing.T) {
for k := range archToIPK {
t.Run(k, func(t *testing.T) {
info := exampleInfo()
info.Arch = k
info = ensureValidArch(info)
require.Equal(t, archToIPK[k], info.Arch)
})
}
t.Run("override", func(t *testing.T) {
info := exampleInfo()
info.IPK.Arch = "foo64"
info = ensureValidArch(info)
require.Equal(t, "foo64", info.Arch)
})
}
func TestFields(t *testing.T) {
var w bytes.Buffer
require.NoError(t, renderControl(&w, controlData{
Info: nfpm.WithDefaults(&nfpm.Info{
Name: "foo",
Description: "Foo does things",
Priority: "extra",
Maintainer: "Carlos A Becker <pkg@carlosbecker.com>",
Version: "v1.0.0",
Section: "default",
Homepage: "http://carlosbecker.com",
Overridables: nfpm.Overridables{
IPK: nfpm.IPK{
Fields: map[string]string{
"Bugs": "https://github.com/goreleaser/nfpm/issues",
"Empty": "",
},
},
},
}),
InstalledSize: 10,
}))
golden := "testdata/control3.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func TestMost(t *testing.T) {
var w bytes.Buffer
require.NoError(t, renderControl(&w, controlData{
Info: nfpm.WithDefaults(&nfpm.Info{
Name: "foo",
Description: "Foo does things",
Priority: "extra",
Maintainer: "Carlos A Becker <pkg@carlosbecker.com>",
Version: "v1.0.0",
Section: "default",
License: "MIT",
Homepage: "http://carlosbecker.com",
Overridables: nfpm.Overridables{
IPK: nfpm.IPK{
ABIVersion: "1",
AutoInstalled: true,
Essential: true,
Fields: map[string]string{
"Bugs": "https://github.com/goreleaser/nfpm/issues",
"Empty": "",
},
},
},
}),
InstalledSize: 10,
}))
golden := "testdata/control_most.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func TestGlob(t *testing.T) {
require.NoError(t, Default.Package(nfpm.WithDefaults(&nfpm.Info{
Name: "nfpm-repro",
Version: "1.0.0",
Maintainer: "asdfasdf",
Overridables: nfpm.Overridables{
Contents: files.Contents{
{
Destination: "/usr/share/nfpm-repro",
Source: "../files/*",
},
},
},
}), io.Discard))
}
func TestBadProvides(t *testing.T) {
var w bytes.Buffer
info := exampleInfo()
info.Provides = []string{" "}
require.NoError(t, renderControl(&w, controlData{
Info: nfpm.WithDefaults(info),
}))
golden := "testdata/bad_provides.golden"
if *update {
require.NoError(t, os.WriteFile(golden, w.Bytes(), 0o600))
}
bts, err := os.ReadFile(golden) //nolint:gosec
require.NoError(t, err)
require.Equal(t, string(bts), w.String())
}
func Test_stripDisallowedFields(t *testing.T) {
tests := []struct {
description string
info *nfpm.Info
expect map[string]string
}{
{
description: "",
info: &nfpm.Info{
Overridables: nfpm.Overridables{
IPK: nfpm.IPK{
ABIVersion: "1",
AutoInstalled: true,
Essential: true,
Fields: map[string]string{
"Bugs": "https://github.com/goreleaser/nfpm/issues",
"Empty": "",
"Conffiles": "removed",
"Filename": "removed",
"Installed-Time": "removed",
"MD5sum": "removed",
"SHA256sum": "removed",
"Size": "removed",
"size": "removed",
"Status": "removed",
"Source": "ok",
},
},
},
},
expect: map[string]string{
"Bugs": "https://github.com/goreleaser/nfpm/issues",
"Empty": "",
"Source": "ok",
},
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
assert := assert.New(t)
stripDisallowedFields(tc.info)
assert.Equal(tc.expect, tc.info.Overridables.IPK.Fields)
})
}
}

108
ipk/tar.go Normal file
View File

@ -0,0 +1,108 @@
package ipk
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"time"
"github.com/goreleaser/nfpm/v2/files"
)
// newTGZ creates a new tar.gz archive with the given name and populates it
// with the given function.
//
// The function returns the bytes of the archive, its size and an error if any.
func newTGZ(name string, populate func(*tar.Writer) error) ([]byte, error) {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
tarball := tar.NewWriter(gz)
// the writers are properly closed later, this is just in case that we error out
defer gz.Close() // nolint: errcheck
defer tarball.Close() // nolint: errcheck
if err := populate(tarball); err != nil {
return nil, fmt.Errorf("cannot populate '%s': %w", name, err)
}
if err := tarball.Close(); err != nil {
return nil, fmt.Errorf("cannot close '%s': %w", name, err)
}
if err := gz.Close(); err != nil {
return nil, fmt.Errorf("cannot close '%s': %w", name, err)
}
return buf.Bytes(), nil
}
// writeFile writes a file from the filesystem to the tarball.
func writeFile(out *tar.Writer, file *files.Content) (int64, error) {
f, err := os.OpenFile(file.Source, os.O_RDONLY, 0o600) //nolint:gosec
if err != nil {
return 0, fmt.Errorf("could not open file %s to read and include in the archive: %w", file.Source, err)
}
defer f.Close() // nolint: errcheck
header, err := tar.FileInfoHeader(file, file.Source)
if err != nil {
return 0, err
}
content, err := io.ReadAll(f)
if err != nil {
return 0, err
}
size := int64(len(content))
// tar.FileInfoHeader only uses file.Mode().Perm() which masks the mode with
// 0o777 which we don't want because we want to be able to set the suid bit.
header.Mode = int64(file.Mode())
header.Format = tar.FormatGNU
header.Name = files.AsExplicitRelativePath(file.Destination)
header.Size = size
header.Uname = file.FileInfo.Owner
header.Gname = file.FileInfo.Group
if err := out.WriteHeader(header); err != nil {
return 0, fmt.Errorf("cannot write tar header for file %s to archive: %w", file.Source, err)
}
n, err := out.Write(content)
if err != nil {
return 0, fmt.Errorf("%s: failed to copy: %w", file.Source, err)
}
if int64(n) != size {
return 0, fmt.Errorf("%s: failed to copy: expected %d bytes, copied %d", file.Source, size, n)
}
return size, nil
}
// writeToFile writes a file to the tarball where the contents are an array of bytes.
func writeToFile(out *tar.Writer, filename string, content []byte, mtime time.Time) error {
header := tar.Header{
Name: files.AsExplicitRelativePath(filename),
Size: int64(len(content)),
Mode: 0o644,
ModTime: mtime,
Typeflag: tar.TypeReg,
Format: tar.FormatGNU,
}
if err := out.WriteHeader(&header); err != nil {
return fmt.Errorf("cannot write file header %s to archive: %w", header.Name, err)
}
_, err := out.Write(content)
if err != nil {
return fmt.Errorf("cannot write file %s payload: %w", header.Name, err)
}
return nil
}

167
ipk/tar_test.go Normal file
View File

@ -0,0 +1,167 @@
package ipk
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"io"
"path"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_newTGZ(t *testing.T) {
unknownErr := errors.New("unknown error")
tests := []struct {
description string
name string
populate func(*tar.Writer) error
file string
expectedErr error
}{
{
description: "simple",
name: "simple.tar",
populate: func(tw *tar.Writer) error {
return writeToFile(tw, "simple.txt", []byte("hello, world"), time.Now())
},
file: "./simple.txt",
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
got, err := newTGZ(tc.name, tc.populate)
if tc.expectedErr == nil {
require.NoError(err)
require.NotNil(got)
gz, err := gzip.NewReader(bytes.NewReader(got))
require.NoError(err)
require.NotNil(gz)
defer gz.Close() // nolint: errcheck
assert.True(tarContains(t, gz, tc.file))
return
}
require.Error(err)
if !errors.Is(tc.expectedErr, unknownErr) {
assert.ErrorIs(err, tc.expectedErr)
}
})
}
}
func extractFileFromTar(tb testing.TB, tarFile []byte, filename string) []byte {
tb.Helper()
tr := tar.NewReader(bytes.NewReader(tarFile))
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break // End of archive
}
require.NoError(tb, err)
if path.Join("/", hdr.Name) != path.Join("/", filename) {
continue
}
fileContents, err := io.ReadAll(tr)
require.NoError(tb, err)
return fileContents
}
tb.Fatalf("file %q does not exist in tar", filename)
return nil
}
func tarContains(tb testing.TB, r io.Reader, filename string) bool {
tb.Helper()
tr := tar.NewReader(r)
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break // End of archive
}
require.NoError(tb, err)
if path.Join("/", hdr.Name) == path.Join("/", filename) { // nolint:gosec
return true
}
}
return false
}
func tarContents(tb testing.TB, tarFile []byte) []string {
tb.Helper()
contents := []string{}
tr := tar.NewReader(bytes.NewReader(tarFile))
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break // End of archive
}
require.NoError(tb, err)
contents = append(contents, hdr.Name)
}
return contents
}
func getTree(tb testing.TB, tarFile []byte) []string {
tb.Helper()
var result []string
tr := tar.NewReader(bytes.NewReader(tarFile))
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break // End of archive
}
require.NoError(tb, err)
result = append(result, hdr.Name)
}
return result
}
func extractFileHeaderFromTar(tb testing.TB, tarFile []byte, filename string) *tar.Header {
tb.Helper()
tr := tar.NewReader(bytes.NewReader(tarFile))
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break // End of archive
}
require.NoError(tb, err)
if path.Join("/", hdr.Name) != path.Join("/", filename) { // nolint:gosec
continue
}
return hdr
}
tb.Fatalf("file %q does not exist in tar", filename)
return nil
}

15
ipk/testdata/bad_provides.golden vendored Normal file
View File

@ -0,0 +1,15 @@
Architecture: amd64
Description: Foo does things
Maintainer: Carlos A Becker <pkg@carlosbecker.com>
Package: foo
Priority: extra
Version: 1.0.0
Conflicts: zsh
Depends: bash
Homepage: http://carlosbecker.com
Pre-Depends: less
Recommends: git
Replaces: svn
Section: default
Suggests: bash
Vendor: nope

17
ipk/testdata/control.golden vendored Normal file
View File

@ -0,0 +1,17 @@
Architecture: amd64
Description: Foo does things
Maintainer: Carlos A Becker <pkg@carlosbecker.com>
Package: foo
Priority: extra
Version: 1.0.0
Conflicts: zsh
Depends: bash
Homepage: http://carlosbecker.com
Installed-Size: 10
Pre-Depends: less
Provides: bzr
Recommends: git
Replaces: svn
Section: default
Suggests: bash
Vendor: nope

10
ipk/testdata/control2.golden vendored Normal file
View File

@ -0,0 +1,10 @@
Architecture: amd64
Description: Foo does things
Maintainer: Carlos A Becker <pkg@carlosbecker.com>
Package: foo
Priority: extra
Version: 1.0.0
Homepage: http://carlosbecker.com
Installed-Size: 10
Section: default
Vendor: nope

10
ipk/testdata/control3.golden vendored Normal file
View File

@ -0,0 +1,10 @@
Architecture: amd64
Description: Foo does things
Maintainer: Carlos A Becker <pkg@carlosbecker.com>
Package: foo
Priority: extra
Version: 1.0.0
Homepage: http://carlosbecker.com
Installed-Size: 10
Section: default
Bugs: https://github.com/goreleaser/nfpm/issues

10
ipk/testdata/control4.golden vendored Normal file
View File

@ -0,0 +1,10 @@
Architecture: amd64
Description: Foo does things
Maintainer: Carlos A Becker <pkg@carlosbecker.com>
Package: foo
Priority: extra
Version: 1.0.0~beta+meta-2
Homepage: http://carlosbecker.com
Installed-Size: 10
Section: default
Vendor: nope

14
ipk/testdata/control_most.golden vendored Normal file
View File

@ -0,0 +1,14 @@
Architecture: amd64
Description: Foo does things
Maintainer: Carlos A Becker <pkg@carlosbecker.com>
Package: foo
Priority: extra
Version: 1.0.0
ABIVersion: 1
Auto-Installed: yes
Essential: yes
Homepage: http://carlosbecker.com
License: MIT
Installed-Size: 10
Section: default
Bugs: https://github.com/goreleaser/nfpm/issues

6
ipk/testdata/minimal.golden vendored Normal file
View File

@ -0,0 +1,6 @@
Architecture: arm64
Description: Minimal does nothing
Maintainer: maintainer
Package: minimal
Priority: extra
Version: 1.0.0

9
ipk/testdata/multiline.golden vendored Normal file
View File

@ -0,0 +1,9 @@
Architecture: riscv64
Description: This field is a
multiline field
.
that should work.
Maintainer: someone
Package: multiline
Priority: extra
Version: 1.0.0

4
ipk/testdata/templates.golden vendored Normal file
View File

@ -0,0 +1,4 @@
Template: templates/lala
Type: string
Description: Set lala for templates.
For the test purpose this templates.golden is created.

7
ipk/testdata/withepoch.golden vendored Normal file
View File

@ -0,0 +1,7 @@
Architecture: arm64
Description: Has an epoch added to it's version
Maintainer:
Package: withepoch
Priority: extra
Version: 2:1.0.0
Section: default

46
nfpm.go
View File

@ -8,6 +8,7 @@ import (
"io"
"io/fs"
"os"
"sort"
"strings"
"sync"
"time"
@ -59,6 +60,22 @@ func Get(format string) (Packager, error) {
return p, nil
}
// Enumerate lists the available packagers
func Enumerate() []string {
lock.Lock()
defer lock.Unlock()
list := make([]string, 0, len(packagers))
for key := range packagers {
if key != "" {
list = append(list, key)
}
}
sort.Strings(list)
return list
}
// Parse decodes YAML data from an io.Reader into a configuration struct.
func Parse(in io.Reader) (config Config, err error) {
return ParseWithEnvMapping(in, os.Getenv)
@ -252,6 +269,15 @@ func (c *Config) expandEnvVars() {
c.Info.Deb.Fields[k] = os.Expand(v, c.envMappingFunc)
}
c.Info.Deb.Predepends = c.expandEnvVarsStringSlice(c.Info.Deb.Predepends)
// IPK specific
for k, v := range c.Info.IPK.Fields {
c.Info.IPK.Fields[k] = os.Expand(v, c.envMappingFunc)
}
c.Info.IPK.Predepends = c.expandEnvVarsStringSlice(c.Info.IPK.Predepends)
// RPM specific
c.Info.RPM.Packager = os.Expand(c.RPM.Packager, c.envMappingFunc)
}
// Info contains information about a single package.
@ -332,6 +358,7 @@ type Overridables struct {
Deb Deb `yaml:"deb,omitempty" json:"deb,omitempty" jsonschema:"title=deb-specific settings"`
APK APK `yaml:"apk,omitempty" json:"apk,omitempty" jsonschema:"title=apk-specific settings"`
ArchLinux ArchLinux `yaml:"archlinux,omitempty" json:"archlinux,omitempty" jsonschema:"title=archlinux-specific settings"`
IPK IPK `yaml:"ipk,omitempty" json:"ipk,omitempty" jsonschema:"title=ipk-specific settings"`
}
type ArchLinux struct {
@ -440,6 +467,25 @@ type DebScripts struct {
Config string `yaml:"config,omitempty" json:"config,omitempty" jsonschema:"title=config"`
}
// IPK is custom configs that are only available on deb packages.
type IPK struct {
ABIVersion string `yaml:"abi_version,omitempty" json:"abi_version,omitempty" jsonschema:"title=abi version"`
Alternatives []IPKAlternative `yaml:"alternatives,omitempty" json:"alternatives,omitempty" jsonschema:"title=alternatives"`
Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in deb nomenclature"`
AutoInstalled bool `yaml:"auto_installed,omitempty" json:"auto_installed,omitempty" jsonschema:"title=auto installed,default=false"`
Essential bool `yaml:"essential,omitempty" json:"essential,omitempty" jsonschema:"title=whether package is essential,default=false"`
Fields map[string]string `yaml:"fields,omitempty" json:"fields,omitempty" jsonschema:"title=fields"`
Predepends []string `yaml:"predepends,omitempty" json:"predepends,omitempty" jsonschema:"title=predepends directive,example=nfpm"`
Tags []string `yaml:"tags,omitempty" json:"tags,omitempty" jsonschema:"title=tags"`
}
// IPKAlternative represents an alternative for an IPK package.
type IPKAlternative struct {
Priority int `yaml:"priority,omitempty" json:"priority,omitempty" jsonschema:"title=priority"`
Target string `yaml:"target,omitempty" json:"target,omitempty" jsonschema:"title=target"`
LinkName string `yaml:"link_name,omitempty" json:"link_name,omitempty" jsonschema:"title=link name"`
}
// Scripts contains information about maintainer scripts for packages.
type Scripts struct {
PreInstall string `yaml:"preinstall,omitempty" json:"preinstall,omitempty" jsonschema:"title=pre install"`

View File

@ -299,6 +299,7 @@ func TestParseFile(t *testing.T) {
nfpm.RegisterPackager("deb", &fakePackager{})
nfpm.RegisterPackager("rpm", &fakePackager{})
nfpm.RegisterPackager("apk", &fakePackager{})
nfpm.RegisterPackager("ipk", &fakePackager{})
_, err = parseAndValidate("./testdata/overrides.yaml")
require.NoError(t, err)
_, err = parseAndValidate("./testdata/doesnotexist.yaml")

View File

@ -62,6 +62,12 @@ contents:
file_info:
mode: 04755
- packager: ipk
src: ./testdata/fake
dst: /usr/bin/fake2
file_info:
mode: 04755
scripts:
preinstall: ./testdata/acceptance/scripts/preinstall.sh
postinstall: ./testdata/acceptance/scripts/postinstall.sh

View File

@ -49,3 +49,10 @@ overrides:
scripts:
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh
ipk:
depends:
- bash
- fish
scripts:
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh

BIN
testdata/acceptance/dummy.ipk vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,18 @@
name: "foo"
arch: "all"
platform: "linux"
version: "v1.0.0"
maintainer: "John Doe <john@example.com>"
description: Foo conflicts dummy
contents:
- src: ./testdata/fake
dst: /usr/bin/fake
ipk:
alternatives:
- priority: 100
target: /usr/bin/fake
link_name: /usr/bin/bar
- link_name: /usr/bin/baz
target: /usr/bin/fake
priority: 200

View File

@ -0,0 +1,8 @@
name: "foo"
arch: "all"
platform: "linux"
version: "v1.0.0"
maintainer: "John Doe <john@example.com>"
description: Foo conflicts dummy
conflicts:
- dummy

172
testdata/acceptance/ipk.dockerfile vendored Normal file
View File

@ -0,0 +1,172 @@
FROM openwrt/rootfs AS test_base
ARG package
#ARG CACHEBUST=1
RUN mkdir -p /var/lock && \
mkdir -p /var/run && \
mkdir -p /tmp
RUN echo "${package}"
COPY ${package} /tmp/foo.ipk
# ---- minimal test ----
FROM test_base AS min
RUN opkg install /tmp/foo.ipk
# ---- symlink test ----
FROM min AS symlink
RUN ls -l /path/to/symlink | grep "/path/to/symlink -> /etc/foo/whatever.conf"
# ---- simple test ----
FROM min AS simple
RUN test -e /usr/bin/fake
RUN test -f /etc/foo/whatever.conf
RUN echo wat >> /etc/foo/whatever.conf
RUN opkg remove foo
RUN test -f /etc/foo/whatever.conf
RUN test ! -f /usr/bin/fake
# ---- no-glob test ----
FROM min AS no-glob
RUN test -d /usr/share/whatever/
RUN test -f /usr/share/whatever/file1
RUN test -f /usr/share/whatever/file2
RUN test -d /usr/share/whatever/folder2
RUN test -f /usr/share/whatever/folder2/file1
RUN test -f /usr/share/whatever/folder2/file2
# ---- complex test ----
FROM test_base AS complex
RUN opkg install coreutils-stat
RUN test "$(opkg status fish)" = ""
RUN opkg install /tmp/foo.ipk > install.log
RUN opkg depends foo | grep "bash"
RUN cat install.log | grep "package foo suggests installing zsh"
RUN test "$(opkg status fish)" != ""
RUN opkg info foo | grep "Provides: fake"
RUN test -e /usr/bin/fake
RUN test -f /etc/foo/whatever.conf
RUN test -d /usr/share/whatever/
RUN test -d /usr/share/whatever/folder
RUN test -f /usr/share/whatever/folder/file1
RUN test -f /usr/share/whatever/folder/file2
RUN test -d /usr/share/whatever/folder/folder2
RUN test -f /usr/share/whatever/folder/folder2/file1
RUN test -f /usr/share/whatever/folder/folder2/file2
RUN test -d /var/log/whatever
RUN test -d /usr/share/foo
RUN test -d /usr/foo/bar/something
RUN test -d /etc/something
RUN test -f /etc/something/a
RUN test -f /etc/something/b
RUN test -d /etc/something/c
RUN test -f /etc/something/c/d
RUN test $(stat -c %a /usr/bin/fake2) -eq 4755
RUN test -f /tmp/preinstall-proof
RUN test -f /tmp/postinstall-proof
RUN test ! -f /tmp/preremove-proof
RUN test ! -f /tmp/postremove-proof
RUN echo wat >> /etc/foo/whatever.conf
RUN opkg remove foo
RUN test -f /etc/foo/whatever.conf
RUN test ! -f /usr/bin/fake
RUN test ! -f /usr/bin/fake2
RUN test -f /tmp/preremove-proof
RUN test -f /tmp/postremove-proof
# ---- overrides test ----
FROM min AS overrides
RUN test -e /usr/bin/fake
RUN test -f /etc/foo/whatever.conf
RUN test ! -f /tmp/preinstall-proof
RUN test -f /tmp/postinstall-proof
RUN test ! -f /tmp/preremove-proof
RUN test ! -f /tmp/postremove-proof
RUN echo wat >> /etc/foo/whatever.conf
RUN opkg remove foo
RUN test -f /etc/foo/whatever.conf
RUN test ! -f /usr/bin/fake
RUN test -f /tmp/preremove-proof
RUN test ! -f /tmp/postremove-proof
# ---- meta test ----
FROM test_base AS meta
RUN opkg install /tmp/foo.ipk
RUN command -v zsh
# ---- env-var-version test ----
FROM min AS env-var-version
ENV EXPECTVER="Version: 1.0.0~0.1.b1+git.abcdefgh"
RUN opkg info foo | grep "Version" > found
RUN export FOUND_VER="$(cat found)" && \
echo "Expected: '${EXPECTVER}' :: Found: '${FOUND_VER}'" && \
test "${FOUND_VER}" = "${EXPECTVER}"
# ---- changelog test ----
FROM test_base AS withchangelog
# ---- signed test ----
FROM test_base AS signed
# ----- IPK Specific Tests -----
# ---- alternatives test ----
FROM test_base AS alternatives
RUN test ! -e /usr/bin/foo
RUN test ! -e /usr/bin/bar
RUN test ! -e /usr/bin/baz
RUN opkg install /tmp/foo.ipk
RUN test -e /usr/bin/fake
RUN test -e /usr/bin/bar
RUN test -e /usr/bin/baz
# ---- conflicts test ----
FROM test_base AS conflicts
COPY dummy.ipk /tmp/dummy.ipk
# install dummy package
RUN opkg install /tmp/dummy.ipk
# make sure foo can't be installed
RUN opkg install /tmp/foo.ipk 2>&1 | grep "Cannot install package foo"
# make sure foo can be installed if dummy is not installed
RUN opkg remove dummy
RUN opkg install /tmp/foo.ipk
# ---- predepends test ----
FROM test_base AS predepends
COPY dummy.ipk /tmp/dummy.ipk
RUN opkg install /tmp/foo.ipk 2>&1 | grep "cannot find dependency dummy for foo"
RUN opkg install /tmp/dummy.ipk
RUN opkg install /tmp/foo.ipk
# ---- upgrade test ----
FROM test_base AS upgrade
ARG oldpackage
RUN echo "${oldpackage}"
COPY ${oldpackage} /tmp/old_foo.ipk
RUN opkg install /tmp/old_foo.ipk
RUN test -f /tmp/preinstall-proof
RUN cat /tmp/preinstall-proof | grep "Install"
RUN test -f /tmp/postinstall-proof
RUN cat /tmp/postinstall-proof | grep "Install"
# The upgrade process doesn't allow a local upgrade.
RUN opkg install /tmp/foo.ipk
RUN test -f /tmp/preremove-proof
RUN cat /tmp/preremove-proof | grep "Upgrade"
RUN test -f /tmp/postremove-proof
RUN cat /tmp/postremove-proof | grep "Upgrade"
RUN test -f /tmp/preinstall-proof
RUN cat /tmp/preinstall-proof | grep "Upgrade"
# The upgrade process doesn't allow a local upgrade,
# so the following test will fail.
#RUN test -d /tmp/postinstall-proof
#RUN cat /tmp/postinstall-proof | grep "Upgrade"

View File

@ -0,0 +1,9 @@
name: "foo"
arch: "all"
platform: "linux"
version: "v1.0.0"
maintainer: "John Doe <john@example.com>"
description: Foo breaks dummy
ipk:
predepends:
- dummy