From 22106950918095ce206c6092d009c9253adc9520 Mon Sep 17 00:00:00 2001 From: schmidtw Date: Mon, 29 Apr 2024 21:07:01 -0700 Subject: [PATCH] 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. --- .github/workflows/build.yml | 2 +- acceptance_test.go | 34 + cmd/nfpm/main.go | 2 +- internal/cmd/package.go | 10 +- internal/cmd/root.go | 5 +- ipk/ipk.go | 512 ++++++++++ ipk/ipk_test.go | 1035 +++++++++++++++++++++ ipk/tar.go | 108 +++ ipk/tar_test.go | 167 ++++ ipk/testdata/bad_provides.golden | 15 + ipk/testdata/control.golden | 17 + ipk/testdata/control2.golden | 10 + ipk/testdata/control3.golden | 10 + ipk/testdata/control4.golden | 10 + ipk/testdata/control_most.golden | 14 + ipk/testdata/minimal.golden | 6 + ipk/testdata/multiline.golden | 9 + ipk/testdata/templates.golden | 4 + ipk/testdata/withepoch.golden | 7 + nfpm.go | 46 + nfpm_test.go | 1 + testdata/acceptance/core.complex.yaml | 6 + testdata/acceptance/core.overrides.yaml | 7 + testdata/acceptance/dummy.ipk | Bin 0 -> 456 bytes testdata/acceptance/ipk.alternatives.yaml | 18 + testdata/acceptance/ipk.conflicts.yaml | 8 + testdata/acceptance/ipk.dockerfile | 172 ++++ testdata/acceptance/ipk.predepends.yaml | 9 + 28 files changed, 2237 insertions(+), 7 deletions(-) create mode 100644 ipk/ipk.go create mode 100644 ipk/ipk_test.go create mode 100644 ipk/tar.go create mode 100644 ipk/tar_test.go create mode 100644 ipk/testdata/bad_provides.golden create mode 100644 ipk/testdata/control.golden create mode 100644 ipk/testdata/control2.golden create mode 100644 ipk/testdata/control3.golden create mode 100644 ipk/testdata/control4.golden create mode 100644 ipk/testdata/control_most.golden create mode 100644 ipk/testdata/minimal.golden create mode 100644 ipk/testdata/multiline.golden create mode 100644 ipk/testdata/templates.golden create mode 100644 ipk/testdata/withepoch.golden create mode 100644 testdata/acceptance/dummy.ipk create mode 100644 testdata/acceptance/ipk.alternatives.yaml create mode 100644 testdata/acceptance/ipk.conflicts.yaml create mode 100644 testdata/acceptance/ipk.dockerfile create mode 100644 testdata/acceptance/ipk.predepends.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c185e9..12a7683 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/acceptance_test.go b/acceptance_test.go index 02796fc..7e934f5 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -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"} { diff --git a/cmd/nfpm/main.go b/cmd/nfpm/main.go index 28462c7..d98095c 100644 --- a/cmd/nfpm/main.go +++ b/cmd/nfpm/main.go @@ -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 != "" { diff --git a/internal/cmd/package.go b/internal/cmd/package.go index 0bac1b6..6187156 100644 --- a/internal/cmd/package.go +++ b/internal/cmd/package.go @@ -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, )) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index dd3097f..a58b2bb 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -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, diff --git a/ipk/ipk.go b/ipk/ipk.go new file mode 100644 index 0000000..14be0ab --- /dev/null +++ b/ipk/ipk.go @@ -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 " + } +} + +// 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) +} diff --git a/ipk/ipk_test.go b/ipk/ipk_test.go new file mode 100644 index 0000000..67402c5 --- /dev/null +++ b/ipk/ipk_test.go @@ -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 ", + 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 ", + 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 ", + 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 ", + 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 ", + 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 ", 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 ", + 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 ", + 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) + }) + } +} diff --git a/ipk/tar.go b/ipk/tar.go new file mode 100644 index 0000000..fb9f58f --- /dev/null +++ b/ipk/tar.go @@ -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 +} diff --git a/ipk/tar_test.go b/ipk/tar_test.go new file mode 100644 index 0000000..113ed08 --- /dev/null +++ b/ipk/tar_test.go @@ -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 +} diff --git a/ipk/testdata/bad_provides.golden b/ipk/testdata/bad_provides.golden new file mode 100644 index 0000000..8b2faaf --- /dev/null +++ b/ipk/testdata/bad_provides.golden @@ -0,0 +1,15 @@ +Architecture: amd64 +Description: Foo does things +Maintainer: Carlos A Becker +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 diff --git a/ipk/testdata/control.golden b/ipk/testdata/control.golden new file mode 100644 index 0000000..d7a31a8 --- /dev/null +++ b/ipk/testdata/control.golden @@ -0,0 +1,17 @@ +Architecture: amd64 +Description: Foo does things +Maintainer: Carlos A Becker +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 diff --git a/ipk/testdata/control2.golden b/ipk/testdata/control2.golden new file mode 100644 index 0000000..c425cd7 --- /dev/null +++ b/ipk/testdata/control2.golden @@ -0,0 +1,10 @@ +Architecture: amd64 +Description: Foo does things +Maintainer: Carlos A Becker +Package: foo +Priority: extra +Version: 1.0.0 +Homepage: http://carlosbecker.com +Installed-Size: 10 +Section: default +Vendor: nope diff --git a/ipk/testdata/control3.golden b/ipk/testdata/control3.golden new file mode 100644 index 0000000..f9a5298 --- /dev/null +++ b/ipk/testdata/control3.golden @@ -0,0 +1,10 @@ +Architecture: amd64 +Description: Foo does things +Maintainer: Carlos A Becker +Package: foo +Priority: extra +Version: 1.0.0 +Homepage: http://carlosbecker.com +Installed-Size: 10 +Section: default +Bugs: https://github.com/goreleaser/nfpm/issues diff --git a/ipk/testdata/control4.golden b/ipk/testdata/control4.golden new file mode 100644 index 0000000..bece21b --- /dev/null +++ b/ipk/testdata/control4.golden @@ -0,0 +1,10 @@ +Architecture: amd64 +Description: Foo does things +Maintainer: Carlos A Becker +Package: foo +Priority: extra +Version: 1.0.0~beta+meta-2 +Homepage: http://carlosbecker.com +Installed-Size: 10 +Section: default +Vendor: nope diff --git a/ipk/testdata/control_most.golden b/ipk/testdata/control_most.golden new file mode 100644 index 0000000..2ecfa92 --- /dev/null +++ b/ipk/testdata/control_most.golden @@ -0,0 +1,14 @@ +Architecture: amd64 +Description: Foo does things +Maintainer: Carlos A Becker +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 diff --git a/ipk/testdata/minimal.golden b/ipk/testdata/minimal.golden new file mode 100644 index 0000000..6fcab83 --- /dev/null +++ b/ipk/testdata/minimal.golden @@ -0,0 +1,6 @@ +Architecture: arm64 +Description: Minimal does nothing +Maintainer: maintainer +Package: minimal +Priority: extra +Version: 1.0.0 diff --git a/ipk/testdata/multiline.golden b/ipk/testdata/multiline.golden new file mode 100644 index 0000000..d22d9c0 --- /dev/null +++ b/ipk/testdata/multiline.golden @@ -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 diff --git a/ipk/testdata/templates.golden b/ipk/testdata/templates.golden new file mode 100644 index 0000000..2bb149b --- /dev/null +++ b/ipk/testdata/templates.golden @@ -0,0 +1,4 @@ +Template: templates/lala +Type: string +Description: Set lala for templates. + For the test purpose this templates.golden is created. diff --git a/ipk/testdata/withepoch.golden b/ipk/testdata/withepoch.golden new file mode 100644 index 0000000..87d3fcc --- /dev/null +++ b/ipk/testdata/withepoch.golden @@ -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 diff --git a/nfpm.go b/nfpm.go index 62c9a1a..970dd0d 100644 --- a/nfpm.go +++ b/nfpm.go @@ -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"` diff --git a/nfpm_test.go b/nfpm_test.go index 1a84bee..7880d79 100644 --- a/nfpm_test.go +++ b/nfpm_test.go @@ -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") diff --git a/testdata/acceptance/core.complex.yaml b/testdata/acceptance/core.complex.yaml index a100bbf..31216e4 100644 --- a/testdata/acceptance/core.complex.yaml +++ b/testdata/acceptance/core.complex.yaml @@ -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 diff --git a/testdata/acceptance/core.overrides.yaml b/testdata/acceptance/core.overrides.yaml index caebe23..c51d06a 100644 --- a/testdata/acceptance/core.overrides.yaml +++ b/testdata/acceptance/core.overrides.yaml @@ -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 diff --git a/testdata/acceptance/dummy.ipk b/testdata/acceptance/dummy.ipk new file mode 100644 index 0000000000000000000000000000000000000000..ea6c5233629fff3202e42a2ff885b529bdc846f0 GIT binary patch literal 456 zcmV;(0XP01iwFP!00000|I*V>NlnU3%+pQE%u6h)WS|8wFfcGMGcf_v1_lP`w1I(v z34@`DnW3?{fw8%<8H0hLp}DyUgMtA~T!RKmi%SxV6ci{n2itC{7(-c-^~GG{C{)#pw|%t0k#M8 zK8t*2Dc$sTn}CZ*l~9XqC4b>Hx65aYd=J)E8+NqE*?5TFteL)3#rWib_;|}cPm7S- zjxKL*nT5N=gx^UFf4S|lZ@=&T649(T-*Z`aJe<8xr+5?7hsSoW&;J!Jf8;j*#8TH2 z4ZM!LCe|O9Ke3dOn3idE_~5Sa-#)Jn{+h9-Uxn>mWsiZu!LQu?AE(`WQ^FNuJumOd z^Gara`Ri95ZMyx_ndjWTwLs4EnT^Bsh{eBN>&C2`^Z)Th_F3k$KgRwO{Hpc8>iQe+ zfA!9tae*R7Q>*wH82gv(V(Nbr^!#sVVqiL&|LFk` y`9Dd5b#a17PXq%8D8J~f-};_`WuRR*3P!;w7zLw%RsawH0RR8nDQ@ck5C8zj<>}P` literal 0 HcmV?d00001 diff --git a/testdata/acceptance/ipk.alternatives.yaml b/testdata/acceptance/ipk.alternatives.yaml new file mode 100644 index 0000000..329738a --- /dev/null +++ b/testdata/acceptance/ipk.alternatives.yaml @@ -0,0 +1,18 @@ +name: "foo" +arch: "all" +platform: "linux" +version: "v1.0.0" +maintainer: "John Doe " +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 \ No newline at end of file diff --git a/testdata/acceptance/ipk.conflicts.yaml b/testdata/acceptance/ipk.conflicts.yaml new file mode 100644 index 0000000..a111696 --- /dev/null +++ b/testdata/acceptance/ipk.conflicts.yaml @@ -0,0 +1,8 @@ +name: "foo" +arch: "all" +platform: "linux" +version: "v1.0.0" +maintainer: "John Doe " +description: Foo conflicts dummy +conflicts: + - dummy diff --git a/testdata/acceptance/ipk.dockerfile b/testdata/acceptance/ipk.dockerfile new file mode 100644 index 0000000..18ec488 --- /dev/null +++ b/testdata/acceptance/ipk.dockerfile @@ -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" diff --git a/testdata/acceptance/ipk.predepends.yaml b/testdata/acceptance/ipk.predepends.yaml new file mode 100644 index 0000000..886178a --- /dev/null +++ b/testdata/acceptance/ipk.predepends.yaml @@ -0,0 +1,9 @@ +name: "foo" +arch: "all" +platform: "linux" +version: "v1.0.0" +maintainer: "John Doe " +description: Foo breaks dummy +ipk: + predepends: + - dummy