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:
parent
409b51628a
commit
2210695091
|
@ -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:
|
||||
|
|
|
@ -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"} {
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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,
|
||||
))
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
Architecture: arm64
|
||||
Description: Minimal does nothing
|
||||
Maintainer: maintainer
|
||||
Package: minimal
|
||||
Priority: extra
|
||||
Version: 1.0.0
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
Template: templates/lala
|
||||
Type: string
|
||||
Description: Set lala for templates.
|
||||
For the test purpose this templates.golden is created.
|
|
@ -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
46
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"`
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Binary file not shown.
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
Loading…
Reference in New Issue