1
1
Fork 0
mirror of https://github.com/goreleaser/nfpm synced 2024-05-18 08:16:22 +02:00

feat: add support for Arch Linux packages (#543)

* feat: add support for Arch Linux packages

* test: Add initial tests

* test: Increase coverage by modifying example info

* test: Add test for ArchLinux.ConventionalFileName()

* docs: Return error if package name is invalid

* fix: Make empty name invalid

* fix: Add replaces field to .PKGINFO generator

* test: Add additional tests

* test: Test for added replaces field

* docs: Add more comments

* style: Run gofumpt

* fix: Handle errors as recommended by linter

* fix: Allow changing the pkgbase

* style: Resolve semgrep findings

* docs: Change docs to reflect new Arch Linux packager

* docs: Fix spelling mistake in comment

Co-authored-by: Dj Gilcrease <digitalxero@gmail.com>

* docs: use aspell to fix all spelling mistakes

* feat: Handle packaging formats with non-distinct file extensions as described in #546

* fix: Add newline to generated .INSTALL file

* fix: Take into account provided info for non-symlink files

* docs: Fix names for arch-specific scripts in documentation

* fix: Only consider files with the correct packager field

* fix: Use correct scripts field for post_remove script

* test: Implement archlinux acceptance tests

* test: Add archlinux to acceptance_test.go

* test: Add archlinux to github test matrix

* test: Use updated build.yml from main branch

* Fix ConventionalExtension() for apk

* fix: Take epoch value into account

* fix: Add arm5 and arm6 architectures

Co-authored-by: Dj Gilcrease <digitalxero@gmail.com>
Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Arsen6331 2022-10-15 17:54:36 +00:00 committed by GitHub
parent 1a66c73f3a
commit a18661b627
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1201 additions and 8 deletions

View File

@ -64,7 +64,7 @@ jobs:
acceptance-tests:
strategy:
matrix:
pkgFormat: [ deb, rpm, apk ]
pkgFormat: [ deb, rpm, apk, archlinux ]
pkgPlatform: [ amd64, arm64, 386, ppc64le, armv6, armv7, s390x ]
runs-on: ubuntu-latest
env:
@ -88,4 +88,3 @@ jobs:
run: ./scripts/test.sh acceptance ubuntu-latest
env:
TEST_PATTERN: "/${{ matrix.pkgFormat }}/${{ matrix.pkgPlatform }}/"

View File

@ -13,6 +13,7 @@ import (
"github.com/goreleaser/nfpm/v2"
_ "github.com/goreleaser/nfpm/v2/apk"
_ "github.com/goreleaser/nfpm/v2/arch"
_ "github.com/goreleaser/nfpm/v2/deb"
_ "github.com/goreleaser/nfpm/v2/rpm"
"github.com/stretchr/testify/require"
@ -20,9 +21,10 @@ import (
// nolint: gochecknoglobals
var formatArchs = map[string][]string{
"apk": {"amd64", "arm64", "386", "ppc64le", "armv6", "armv7", "s390x"},
"deb": {"amd64", "arm64", "ppc64le", "armv7", "s390x"},
"rpm": {"amd64", "arm64", "ppc64le", "armv7"},
"apk": {"amd64", "arm64", "386", "ppc64le", "armv6", "armv7", "s390x"},
"deb": {"amd64", "arm64", "ppc64le", "armv7", "s390x"},
"rpm": {"amd64", "arm64", "ppc64le", "armv7"},
"archlinux": {"amd64"},
}
func TestCore(t *testing.T) {

View File

@ -101,6 +101,11 @@ func (a *Apk) ConventionalFileName(info *nfpm.Info) string {
return fmt.Sprintf("%s_%s_%s.apk", info.Name, version, info.Arch)
}
// ConventionalExtension returns the file name conventionally used for Apk packages
func (*Apk) ConventionalExtension() string {
return ".apk"
}
// Package writes a new apk package to the given writer using the given info.
func (*Apk) Package(info *nfpm.Info, apk io.Writer) (err error) {
info = ensureValidArch(info)

693
arch/arch.go Normal file
View File

@ -0,0 +1,693 @@
// Package arch implements nfpm.Packager providing bindings for Arch Linux packages.
package arch
import (
"archive/tar"
"bytes"
"crypto/md5"
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"github.com/klauspost/compress/zstd"
"github.com/klauspost/pgzip"
)
var ErrInvalidPkgName = errors.New("archlinux: package names may only contain alphanumeric characters or one of ., _, +, or -, and may not start with hyphen or dot")
const packagerName = "archlinux"
// nolint: gochecknoinits
func init() {
nfpm.RegisterPackager(packagerName, Default)
}
// Default ArchLinux packager.
// nolint: gochecknoglobals
var Default = ArchLinux{}
type ArchLinux struct{}
// nolint: gochecknoglobals
var archToArchLinux = map[string]string{
"all": "any",
"amd64": "x86_64",
"386": "i686",
"arm64": "aarch64",
"arm7": "armv7h",
"arm6": "armv6h",
"arm5": "arm",
}
func ensureValidArch(info *nfpm.Info) *nfpm.Info {
if info.ArchLinux.Arch != "" {
info.Arch = info.ArchLinux.Arch
} else if arch, ok := archToArchLinux[info.Arch]; ok {
info.Arch = arch
}
return info
}
// ConventionalFileName returns a file name for a package conforming
// to Arch Linux package naming guidelines. See:
// https://wiki.archlinux.org/title/Arch_package_guidelines#Package_naming
func (ArchLinux) ConventionalFileName(info *nfpm.Info) string {
info = ensureValidArch(info)
pkgrel, err := strconv.Atoi(info.Release)
if err != nil {
pkgrel = 1
}
name := fmt.Sprintf(
"%s-%s-%d-%s.pkg.tar.zst",
info.Name,
info.Version,
pkgrel,
info.Arch,
)
return validPkgName(name)
}
// validPkgName removes any invalid characters from a string
func validPkgName(s string) string {
s = strings.Map(mapValidChar, s)
s = strings.TrimLeft(s, "-.")
return s
}
// nameIsValid checks whether a package name is valid
func nameIsValid(s string) bool {
return s != "" && s == validPkgName(s)
}
// mapValidChar returns r if it is allowed, otherwise, returns -1
func mapValidChar(r rune) rune {
if r >= 'a' && r <= 'z' ||
r >= 'A' && r <= 'Z' ||
r >= '0' && r <= '9' ||
isOneOf(r, '.', '_', '+', '-') {
return r
}
return -1
}
// isOneOf checks whether a rune is one of the runes in rr
func isOneOf(r rune, rr ...rune) bool {
for _, char := range rr {
if r == char {
return true
}
}
return false
}
// Package writes a new archlinux package to the given writer using the given info.
func (ArchLinux) Package(info *nfpm.Info, w io.Writer) error {
if !nameIsValid(info.Name) {
return ErrInvalidPkgName
}
zw, err := zstd.NewWriter(w)
if err != nil {
return err
}
defer zw.Close()
tw := tar.NewWriter(zw)
defer tw.Close()
entries, totalSize, err := createFilesInTar(info, tw)
if err != nil {
return err
}
pkginfoEntry, err := createPkginfo(info, tw, totalSize)
if err != nil {
return err
}
// .PKGINFO must be the first entry in .MTREE
entries = append([]MtreeEntry{*pkginfoEntry}, entries...)
err = createMtree(info, tw, entries)
if err != nil {
return err
}
return createScripts(info, tw)
}
// ConventionalExtension returns the file name conventionally used for Arch Linux packages
func (ArchLinux) ConventionalExtension() string {
return ".pkg.tar.zst"
}
// createFilesInTar adds the files described in the given info to the given tar writer
func createFilesInTar(info *nfpm.Info, tw *tar.Writer) ([]MtreeEntry, int64, error) {
created := map[string]struct{}{}
var entries []MtreeEntry
var totalSize int64
for _, content := range info.Contents {
if content.Packager != "" && content.Packager != packagerName {
continue
}
path := normalizePath(content.Destination)
switch content.Type {
case "ghost":
// Ignore ghost files
case "dir":
err := createDirs(content.Destination, tw, created)
if err != nil {
return nil, 0, err
}
modtime := time.Now()
// If the time is given, use it
if content.FileInfo != nil && !content.ModTime().IsZero() {
modtime = content.ModTime()
}
entries = append(entries, MtreeEntry{
Destination: path,
Time: modtime.Unix(),
Type: content.Type,
})
case "symlink":
dir := filepath.Dir(path)
err := createDirs(dir, tw, created)
if err != nil {
return nil, 0, err
}
modtime := time.Now()
// If the time is given, use it
if content.FileInfo != nil && !content.ModTime().IsZero() {
modtime = content.ModTime()
}
err = tw.WriteHeader(&tar.Header{
Name: normalizePath(content.Destination),
Linkname: content.Source,
ModTime: modtime,
Typeflag: tar.TypeSymlink,
})
if err != nil {
return nil, 0, err
}
entries = append(entries, MtreeEntry{
LinkSource: content.Source,
Destination: path,
Time: modtime.Unix(),
Mode: 0o777,
Type: content.Type,
})
default:
dir := filepath.Dir(path)
err := createDirs(dir, tw, created)
if err != nil {
return nil, 0, err
}
src, err := os.Open(content.Source)
if err != nil {
return nil, 0, err
}
srcFi, err := src.Stat()
if err != nil {
return nil, 0, err
}
header := &tar.Header{
Name: path,
Mode: int64(srcFi.Mode()),
Typeflag: tar.TypeReg,
Size: srcFi.Size(),
ModTime: srcFi.ModTime(),
}
if content.FileInfo != nil && content.Mode() != 0 {
header.Mode = int64(content.Mode())
}
if content.FileInfo != nil && !content.ModTime().IsZero() {
header.ModTime = content.ModTime()
}
if content.FileInfo != nil && content.Size() != 0 {
header.Size = content.Size()
}
err = tw.WriteHeader(header)
if err != nil {
return nil, 0, err
}
sha256Hash := sha256.New()
md5Hash := md5.New()
w := io.MultiWriter(tw, sha256Hash, md5Hash)
_, err = io.Copy(w, src)
if err != nil {
return nil, 0, err
}
entries = append(entries, MtreeEntry{
Destination: path,
Time: srcFi.ModTime().Unix(),
Mode: int64(srcFi.Mode()),
Size: srcFi.Size(),
Type: content.Type,
MD5: md5Hash.Sum(nil),
SHA256: sha256Hash.Sum(nil),
})
totalSize += srcFi.Size()
}
}
return entries, totalSize, nil
}
func createDirs(dst string, tw *tar.Writer, created map[string]struct{}) error {
for _, path := range neededPaths(dst) {
path = normalizePath(path) + "/"
if _, ok := created[path]; ok {
continue
}
err := tw.WriteHeader(&tar.Header{
Name: path,
Mode: 0o755,
Typeflag: tar.TypeDir,
ModTime: time.Now(),
Uname: "root",
Gname: "root",
})
if err != nil {
return fmt.Errorf("failed to create folder: %w", err)
}
created[path] = struct{}{}
}
return nil
}
func defaultStr(s, def string) string {
if s == "" {
return def
}
return s
}
func neededPaths(dst string) []string {
dst = files.ToNixPath(dst)
split := strings.Split(strings.Trim(dst, "/."), "/")
var sb strings.Builder
var paths []string
for index, elem := range split {
if index != 0 {
sb.WriteRune('/')
}
sb.WriteString(elem)
paths = append(paths, sb.String())
}
return paths
}
func normalizePath(src string) string {
return files.ToNixPath(strings.TrimPrefix(src, "/"))
}
func createPkginfo(info *nfpm.Info, tw *tar.Writer, totalSize int64) (*MtreeEntry, error) {
if !nameIsValid(info.Name) {
return nil, ErrInvalidPkgName
}
buf := &bytes.Buffer{}
info = ensureValidArch(info)
pkgrel, err := strconv.Atoi(info.Release)
if err != nil {
pkgrel = 1
}
pkgver := fmt.Sprintf("%s-%d", info.Version, pkgrel)
if info.Epoch != "" {
epoch, err := strconv.ParseUint(info.Epoch, 10, 64)
if err == nil {
pkgver = fmt.Sprintf("%d:%s-%d", epoch, info.Version, pkgrel)
}
}
// Description cannot contain newlines
pkgdesc := strings.ReplaceAll(info.Description, "\n", " ")
_, err = io.WriteString(buf, "# Generated by nfpm\n")
if err != nil {
return nil, err
}
builddate := strconv.FormatInt(time.Now().Unix(), 10)
totalSizeStr := strconv.FormatInt(totalSize, 10)
err = writeKVPairs(buf, map[string]string{
"size": totalSizeStr,
"pkgname": info.Name,
"pkgbase": defaultStr(info.ArchLinux.Pkgbase, info.Name),
"pkgver": pkgver,
"pkgdesc": pkgdesc,
"url": info.Homepage,
"builddate": builddate,
"packager": defaultStr(info.ArchLinux.Packager, "Unknown Packager"),
"arch": info.Arch,
"license": info.License,
})
if err != nil {
return nil, err
}
for _, replaces := range info.Replaces {
err = writeKVPair(buf, "replaces", replaces)
if err != nil {
return nil, err
}
}
for _, conflict := range info.Conflicts {
err = writeKVPair(buf, "conflict", conflict)
if err != nil {
return nil, err
}
}
for _, provides := range info.Provides {
err = writeKVPair(buf, "provides", provides)
if err != nil {
return nil, err
}
}
for _, depend := range info.Depends {
err = writeKVPair(buf, "depend", depend)
if err != nil {
return nil, err
}
}
for _, content := range info.Contents {
if content.Type == "config" || content.Type == "config|noreplace" {
path := normalizePath(content.Destination)
path = strings.TrimPrefix(path, "./")
err = writeKVPair(buf, "backup", path)
if err != nil {
return nil, err
}
}
}
size := buf.Len()
err = tw.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Mode: 0o644,
Name: ".PKGINFO",
Size: int64(size),
ModTime: time.Now(),
})
if err != nil {
return nil, err
}
md5Hash := md5.New()
sha256Hash := sha256.New()
r := io.TeeReader(buf, md5Hash)
r = io.TeeReader(r, sha256Hash)
_, err = io.Copy(tw, r)
if err != nil {
return nil, err
}
return &MtreeEntry{
Destination: ".PKGINFO",
Time: time.Now().Unix(),
Mode: 0o644,
Size: int64(size),
Type: "file",
MD5: md5Hash.Sum(nil),
SHA256: sha256Hash.Sum(nil),
}, nil
}
func writeKVPairs(w io.Writer, s map[string]string) error {
for key, val := range s {
err := writeKVPair(w, key, val)
if err != nil {
return err
}
}
return nil
}
func writeKVPair(w io.Writer, key, value string) error {
if value == "" {
return nil
}
_, err := io.WriteString(w, key)
if err != nil {
return err
}
_, err = io.WriteString(w, " = ")
if err != nil {
return err
}
_, err = io.WriteString(w, value)
if err != nil {
return err
}
_, err = io.WriteString(w, "\n")
return err
}
type MtreeEntry struct {
LinkSource string
Destination string
Time int64
Mode int64
Size int64
Type string
MD5 []byte
SHA256 []byte
}
func (me *MtreeEntry) WriteTo(w io.Writer) (int64, error) {
switch me.Type {
case "dir":
n, err := fmt.Fprintf(
w,
"./%s time=%d.0 type=dir\n",
normalizePath(me.Destination),
me.Time,
)
return int64(n), err
case "symlink":
n, err := fmt.Fprintf(
w,
"./%s time=%d.0 mode=%o type=link link=%s\n",
normalizePath(me.Destination),
me.Time,
me.Mode,
me.LinkSource,
)
return int64(n), err
default:
n, err := fmt.Fprintf(
w,
"./%s time=%d.0 mode=%o size=%d type=file md5digest=%x sha256digest=%x\n",
normalizePath(me.Destination),
me.Time,
me.Mode,
me.Size,
me.MD5,
me.SHA256,
)
return int64(n), err
}
}
func createMtree(info *nfpm.Info, tw *tar.Writer, entries []MtreeEntry) error {
buf := &bytes.Buffer{}
gw := pgzip.NewWriter(buf)
defer gw.Close()
created := map[string]struct{}{}
_, err := io.WriteString(gw, "#mtree\n")
if err != nil {
return err
}
for _, entry := range entries {
destDir := filepath.Dir(entry.Destination)
dirs := createDirsMtree(destDir, created)
for _, dir := range dirs {
_, err = dir.WriteTo(gw)
if err != nil {
return err
}
}
_, err = entry.WriteTo(gw)
if err != nil {
return err
}
}
gw.Close()
err = tw.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Mode: 0o644,
Name: ".MTREE",
Size: int64(buf.Len()),
ModTime: time.Now(),
})
if err != nil {
return err
}
_, err = io.Copy(tw, buf)
return err
}
func createDirsMtree(dst string, created map[string]struct{}) []MtreeEntry {
var out []MtreeEntry
for _, path := range neededPaths(dst) {
path = normalizePath(path) + "/"
if path == "./" {
continue
}
if _, ok := created[path]; ok {
continue
}
out = append(out, MtreeEntry{
Destination: path,
Time: time.Now().Unix(),
Mode: 0o755,
Type: "dir",
})
created[path] = struct{}{}
}
return out
}
func createScripts(info *nfpm.Info, tw *tar.Writer) error {
scripts := map[string]string{}
if info.Scripts.PreInstall != "" {
scripts["pre_install"] = info.Scripts.PreInstall
}
if info.Scripts.PostInstall != "" {
scripts["post_install"] = info.Scripts.PostInstall
}
if info.Scripts.PreRemove != "" {
scripts["pre_remove"] = info.Scripts.PreRemove
}
if info.Scripts.PostRemove != "" {
scripts["post_remove"] = info.Scripts.PostRemove
}
if info.ArchLinux.Scripts.PreUpgrade != "" {
scripts["pre_upgrade"] = info.ArchLinux.Scripts.PreUpgrade
}
if info.ArchLinux.Scripts.PostUpgrade != "" {
scripts["post_upgrade"] = info.ArchLinux.Scripts.PostUpgrade
}
if len(scripts) == 0 {
return nil
}
buf := &bytes.Buffer{}
err := writeScripts(buf, scripts)
if err != nil {
return err
}
err = tw.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Mode: 0o644,
Name: ".INSTALL",
Size: int64(buf.Len()),
ModTime: time.Now(),
})
if err != nil {
return err
}
_, err = io.Copy(tw, buf)
return err
}
func writeScripts(w io.Writer, scripts map[string]string) error {
for script, path := range scripts {
fmt.Fprintf(w, "function %s() {\n", script)
fl, err := os.Open(path)
if err != nil {
return err
}
_, err = io.Copy(w, fl)
if err != nil {
return err
}
fl.Close()
_, err = io.WriteString(w, "\n}\n\n")
if err != nil {
return err
}
}
return nil
}

280
arch/arch_test.go Normal file
View File

@ -0,0 +1,280 @@
package arch
import (
"archive/tar"
"bytes"
"io"
"strings"
"testing"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"github.com/klauspost/pgzip"
"github.com/stretchr/testify/require"
)
func exampleInfo() *nfpm.Info {
return nfpm.WithDefaults(&nfpm.Info{
Name: "foo-test",
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",
License: "MIT",
Overridables: nfpm.Overridables{
Depends: []string{
"bash",
},
Replaces: []string{
"svn",
},
Provides: []string{
"bzr",
},
Conflicts: []string{
"zsh",
},
Contents: []*files.Content{
{
Source: "../testdata/fake",
Destination: "/usr/local/bin/fake",
},
{
Source: "../testdata/whatever.conf",
Destination: "/etc/fake/fake.conf",
Type: "config",
},
{
Destination: "/var/log/whatever",
Type: "dir",
},
{
Destination: "/usr/share/whatever",
Type: "dir",
},
{
Source: "/etc/fake/fake.conf",
Destination: "/etc/fake/fake-link.conf",
Type: "symlink",
},
},
Scripts: nfpm.Scripts{
PreInstall: "../testdata/scripts/preinstall.sh",
PostInstall: "../testdata/scripts/postinstall.sh",
PreRemove: "../testdata/scripts/preremove.sh",
PostRemove: "../testdata/scripts/postremove.sh",
},
ArchLinux: nfpm.ArchLinux{
Scripts: nfpm.ArchLinuxScripts{
PreUpgrade: "../testdata/scripts/preupgrade.sh",
PostUpgrade: "../testdata/scripts/postupgrade.sh",
},
},
},
})
}
func TestArch(t *testing.T) {
for _, arch := range []string{"386", "amd64", "arm64"} {
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 TestArchNoFiles(t *testing.T) {
info := exampleInfo()
info.Contents = nil
info.Scripts = nfpm.Scripts{}
info.ArchLinux = nfpm.ArchLinux{}
err := Default.Package(info, io.Discard)
require.NoError(t, err)
}
func TestArchNoInfo(t *testing.T) {
err := Default.Package(nfpm.WithDefaults(&nfpm.Info{}), io.Discard)
require.Error(t, err)
}
func TestArchConventionalFileName(t *testing.T) {
for _, arch := range []string{"386", "amd64", "arm64"} {
arch := arch
t.Run(arch, func(t *testing.T) {
info := exampleInfo()
info.Arch = arch
name := Default.ConventionalFileName(info)
require.Equal(t,
"foo-test-1.0.0-1-"+archToArchLinux[arch]+".pkg.tar.zst",
name,
)
})
}
}
func TestArchPkginfo(t *testing.T) {
pkginfoData, err := makeTestPkginfo(t, exampleInfo())
require.NoError(t, err)
fields := extractPkginfoFields(pkginfoData)
require.Equal(t, "foo-test", fields["pkgname"])
require.Equal(t, "foo-test", fields["pkgbase"])
require.Equal(t, "1.0.0-1", fields["pkgver"])
require.Equal(t, "Foo does things", fields["pkgdesc"])
require.Equal(t, "http://carlosbecker.com", fields["url"])
require.Equal(t, "Unknown Packager", fields["packager"])
require.Equal(t, "x86_64", fields["arch"])
require.Equal(t, "MIT", fields["license"])
require.Equal(t, "1234", fields["size"])
require.Equal(t, "svn", fields["replaces"])
require.Equal(t, "zsh", fields["conflict"])
require.Equal(t, "bzr", fields["provides"])
require.Equal(t, "bash", fields["depend"])
require.Equal(t, "etc/fake/fake.conf", fields["backup"])
}
func TestArchPkgbase(t *testing.T) {
info := exampleInfo()
info.ArchLinux.Pkgbase = "foo"
pkginfoData, err := makeTestPkginfo(t, info)
require.NoError(t, err)
fields := extractPkginfoFields(pkginfoData)
require.Equal(t, "foo", fields["pkgbase"])
}
func TestArchInvalidName(t *testing.T) {
info := exampleInfo()
info.Name = "#"
_, err := makeTestPkginfo(t, info)
require.ErrorIs(t, err, ErrInvalidPkgName)
}
func TestArchVersionWithRelease(t *testing.T) {
info := exampleInfo()
info.Version = "0.0.1"
info.Release = "4"
pkginfoData, err := makeTestPkginfo(t, info)
require.NoError(t, err)
fields := extractPkginfoFields(pkginfoData)
require.Equal(t, "0.0.1-4", fields["pkgver"])
}
func TestArchVersionWithEpoch(t *testing.T) {
info := exampleInfo()
info.Version = "0.0.1"
info.Epoch = "2"
pkginfoData, err := makeTestPkginfo(t, info)
require.NoError(t, err)
fields := extractPkginfoFields(pkginfoData)
require.Equal(t, "2:0.0.1-1", fields["pkgver"])
}
func TestArchOverrideArchitecture(t *testing.T) {
info := exampleInfo()
info.ArchLinux.Arch = "randomarch"
pkginfoData, err := makeTestPkginfo(t, info)
require.NoError(t, err)
fields := extractPkginfoFields(pkginfoData)
require.Equal(t, "randomarch", fields["arch"])
}
func makeTestPkginfo(t *testing.T, info *nfpm.Info) ([]byte, error) {
t.Helper()
buf := &bytes.Buffer{}
tw := tar.NewWriter(buf)
entry, err := createPkginfo(info, tw, 1234)
if err != nil {
return nil, err
}
tw.Close()
tr := tar.NewReader(buf)
_, err = tr.Next()
require.NoError(t, err)
pkginfoData := make([]byte, entry.Size)
_, err = io.ReadFull(tr, pkginfoData)
if err != nil {
return nil, err
}
return pkginfoData, nil
}
func extractPkginfoFields(data []byte) map[string]string {
strData := string(data)
strData = strings.TrimPrefix(strData, "# Generated by nfpm\n")
strData = strings.TrimSpace(strData)
splitData := strings.Split(strData, "\n")
out := map[string]string{}
for _, kvPair := range splitData {
splitPair := strings.Split(kvPair, " = ")
out[splitPair[0]] = splitPair[1]
}
return out
}
const correctMtree = `#mtree
./foo time=1234.0 type=dir
./3 time=12345.0 mode=644 size=100 type=file md5digest=abcd sha256digest=ef12
./sh time=123456.0 mode=777 type=link link=/bin/bash
`
func TestArchMtree(t *testing.T) {
info := exampleInfo()
buf := &bytes.Buffer{}
tw := tar.NewWriter(buf)
err := createMtree(info, tw, []MtreeEntry{
{
Destination: "/foo",
Time: 1234,
Type: "dir",
},
{
Destination: "/3",
Time: 12345,
Mode: 0o644,
Size: 100,
Type: "file",
MD5: []byte{0xAB, 0xCD},
SHA256: []byte{0xEF, 0x12},
},
{
LinkSource: "/bin/bash",
Destination: "/sh",
Time: 123456,
Mode: 0o777,
Type: "symlink",
},
})
require.NoError(t, err)
tw.Close()
tr := tar.NewReader(buf)
_, err = tr.Next()
require.NoError(t, err)
gr, err := pgzip.NewReader(tr)
require.NoError(t, err)
defer gr.Close()
mtree, err := io.ReadAll(gr)
require.NoError(t, err)
require.InDeltaSlice(t, []byte(correctMtree), mtree, 0)
}

View File

@ -84,6 +84,11 @@ func (*Deb) ConventionalFileName(info *nfpm.Info) string {
return fmt.Sprintf("%s_%s_%s.deb", info.Name, version, info.Arch)
}
// ConventionalExtension returns the file name conventionally used for Deb packages
func (*Deb) ConventionalExtension() string {
return ".deb"
}
// ErrInvalidSignatureType happens if the signature type of a deb is not one of
// origin, maint or archive.
var ErrInvalidSignatureType = errors.New("invalid signature type")

View File

@ -34,7 +34,7 @@ func newPackageCmd() *packageCmd {
cmd.Flags().StringVarP(&root.config, "config", "f", "nfpm.yaml", "config file to be used")
cmd.Flags().StringVarP(&root.target, "target", "t", "", "where to save the generated package (filename, folder or empty for current folder)")
cmd.Flags().StringVarP(&root.packager, "packager", "p", "", "which packager implementation to use [apk|deb|rpm]")
cmd.Flags().StringVarP(&root.packager, "packager", "p", "", "which packager implementation to use [apk|deb|rpm|archlinux]")
root.cmd = cmd
return root

View File

@ -4,6 +4,7 @@ import (
"fmt"
_ "github.com/goreleaser/nfpm/v2/apk"
_ "github.com/goreleaser/nfpm/v2/arch"
_ "github.com/goreleaser/nfpm/v2/deb"
_ "github.com/goreleaser/nfpm/v2/rpm"
"github.com/spf13/cobra"

18
nfpm.go
View File

@ -106,6 +106,11 @@ type Packager interface {
ConventionalFileName(info *Info) string
}
type PackagerWithExtension interface {
Packager
ConventionalExtension() string
}
// Config contains the top level configuration for packages.
type Config struct {
Info `yaml:",inline" json:",inline"`
@ -294,6 +299,19 @@ type Overridables struct {
RPM RPM `yaml:"rpm,omitempty" json:"rpm,omitempty" jsonschema:"title=rpm-specific settings"`
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"`
}
type ArchLinux struct {
Pkgbase string `yaml:"pkgbase,omitempty" json:"pkgbase,omitempty" jsonschema:"title=explicitly specify the name used to refer to a split package, defaults to name"`
Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in archlinux nomenclature"`
Packager string `yaml:"packager,omitempty" json:"packager,omitempty" jsonschema:"title=organization that packaged the software"`
Scripts ArchLinuxScripts `yaml:"scripts,omitempty" json:"scripts,omitempty" jsonschema:"title=archlinux-specific scripts"`
}
type ArchLinuxScripts struct {
PreUpgrade string `yaml:"preupgrade,omitempty" json:"preupgrade,omitempty" jsonschema:"title=preupgrade script"`
PostUpgrade string `yaml:"postupgrade,omitempty" json:"postupgrade,omitempty" jsonschema:"title=postupgrade script"`
}
// RPM is custom configs that are only available on RPM packages.

View File

@ -93,6 +93,11 @@ func (*RPM) ConventionalFileName(info *nfpm.Info) string {
return fmt.Sprintf("%s-%s.%s.rpm", info.Name, version, info.Arch)
}
// ConventionalExtension returns the file name conventionally used for RPM packages
func (*RPM) ConventionalExtension() string {
return ".rpm"
}
// Package writes a new RPM package to the given writer using the given info.
func (*RPM) Package(info *nfpm.Info, w io.Writer) (err error) {
var (

131
testdata/acceptance/archlinux.dockerfile vendored Normal file
View File

@ -0,0 +1,131 @@
FROM archlinux AS test_base
ARG package
RUN echo "${package}"
COPY ${package} /tmp/foo.pkg.tar.zst
# ---- minimal test ----
FROM test_base AS min
RUN pacman --noconfirm -U /tmp/foo.pkg.tar.zst
# ---- 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/local/bin/fake
RUN test -f /etc/foo/whatever.conf
RUN echo wat >> /etc/foo/whatever.conf
RUN pacman --noconfirm -R foo
RUN test -f /etc/foo/whatever.conf.pacsave
RUN test ! -f /usr/local/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 min AS complex
RUN pacman -Qi foo | grep "Depends On\\s*: bash"
RUN pacman -Qi foo | grep "Replaces\\s*: foo"
RUN pacman -Qi foo | grep "Provides\\s*: fake"
RUN test -e /usr/local/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 $(stat -c %a /usr/bin/fake) -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 pacman --noconfirm -R foo
RUN test -f /etc/foo/whatever.conf.pacsave
RUN test ! -f /usr/local/bin/fake
RUN test ! -f /usr/bin/fake
RUN test -f /tmp/preremove-proof
RUN test -f /tmp/postremove-proof
RUN test ! -d /var/log/whatever
RUN test ! -d /usr/share/foo
RUN test ! -d /usr/foo/bar/something
# ---- signed test ----
FROM min AS signed
RUN echo "Arch Linux has no signature support"
# ---- overrides test ----
FROM min AS overrides
RUN test -e /usr/local/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 pacman --noconfirm -R foo
RUN test -f /etc/foo/whatever.conf.pacsave
RUN test ! -f /usr/local/bin/fake
RUN test -f /tmp/preremove-proof
RUN test ! -f /tmp/postremove-proof
# ---- meta test ----
FROM test_base AS meta
RUN pacman -Sy && pacman --noconfirm -U /tmp/foo.pkg.tar.zst
RUN command -v zsh
# ---- env-var-version test ----
FROM min AS env-var-version
ENV EXPECTVER="foo 1.0.0-1"
RUN export FOUND_VER="$(pacman -Q foo)" && \
echo "Expected: '${EXPECTVER}' :: Found: '${FOUND_VER}'" && \
test "${FOUND_VER}" = "${EXPECTVER}"
# ---- changelog test ----
FROM min AS withchangelog
RUN echo "Arch Linux has no changelog support"
# ---- upgrade test ----
FROM test_base AS upgrade
ARG oldpackage
RUN echo "${oldpackage}"
COPY ${oldpackage} /tmp/old_foo.pkg.tar.zst
RUN pacman --noconfirm -U /tmp/old_foo.pkg.tar.zst
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"
RUN test ! -f /tmp/preupgrade-proof
RUN test ! -f /tmp/postupgrade-proof
RUN echo modified > /etc/regular.conf
RUN echo modified > /etc/noreplace.conf
RUN pacman --noconfirm -U /tmp/foo.pkg.tar.zst
RUN test -f /tmp/preupgrade-proof
RUN test -f /tmp/postupgrade-proof
RUN test -f /etc/regular.conf
RUN test -f /etc/regular.conf.pacnew
RUN test -f /etc/noreplace.conf
RUN test -f /etc/noreplace.conf.pacnew

View File

@ -29,8 +29,24 @@ contents:
type: config
- src: ./testdata/fake
dst: /usr/sbin/fake
packager: deb
file_info:
mode: 04755
- src: ./testdata/fake
dst: /usr/sbin/fake
packager: rpm
file_info:
mode: 04755
- src: ./testdata/fake
dst: /usr/sbin/fake
packager: apk
file_info:
mode: 04755
- src: ./testdata/fake
dst: /usr/bin/fake
file_info:
mode: 04755
packager: archlinux
- dst: /usr/foo/bar/something
type: dir
- dst: /var/log/whatever
@ -50,3 +66,7 @@ apk:
scripts:
preupgrade: ./testdata/acceptance/scripts/preupgrade.sh
postupgrade: ./testdata/acceptance/scripts/postupgrade.sh
archlinux:
scripts:
preupgrade: ./testdata/acceptance/scripts/preupgrade.sh
postupgrade: ./testdata/acceptance/scripts/postupgrade.sh

View File

@ -44,3 +44,7 @@ overrides:
scripts:
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh
archlinux:
scripts:
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh

View File

@ -39,3 +39,7 @@ apk:
scripts:
preupgrade: ./testdata/acceptance/scripts/preupgrade.sh
postupgrade: ./testdata/acceptance/scripts/postupgrade.sh
archlinux:
scripts:
preupgrade: ./testdata/acceptance/scripts/preupgrade.sh
postupgrade: ./testdata/acceptance/scripts/postupgrade.sh

View File

@ -39,3 +39,7 @@ apk:
scripts:
preupgrade: ./testdata/acceptance/scripts/preupgrade.sh
postupgrade: ./testdata/acceptance/scripts/postupgrade.sh
archlinux:
scripts:
preupgrade: ./testdata/acceptance/scripts/preupgrade.sh
postupgrade: ./testdata/acceptance/scripts/postupgrade.sh

View File

@ -11,7 +11,7 @@ nfpm package [flags]
```
-f, --config string config file to be used (default "nfpm.yaml")
-h, --help help for package
-p, --packager string which packager implementation to use [apk|deb|rpm]
-p, --packager string which packager implementation to use [apk|deb|rpm|archlinux]
-t, --target string where to save the generated package (filename, folder or empty for current folder)
```

View File

@ -231,6 +231,10 @@ overrides:
# ...
apk:
# ...
archlinux:
depends:
- baz
- some-lib
# Custom configuration applied only to the RPM packager.
rpm:
@ -347,6 +351,24 @@ apk:
key_name: origin
# APK does not use pgp keys, so the key_id field is ignored.
key_id: ignored
archlinux:
# This value is used to specify the name used to refer to a group
# of packages when building a split package. Defaults to name
# See: https://wiki.archlinux.org/title/PKGBUILD#pkgbase
pkgbase: bar
# The packager identifies the organization packaging the software
# rather than the developer. Defaults to "Unknown Packager".
packager: GoReleaser <staff@goreleaser.com>
# Arch Linux specific scripts.
scripts:
# The postupgrade script runs before pacman upgrades the package
preupgrade: ./scripts/preupgrade.sh
# The postupgrade script runs after pacman upgrades the package
postupgrade: ./scripts/postupgrade.sh
```
## Templating

View File

@ -2,7 +2,7 @@
![](/static/banner.svg)
nFPM is a simple, 0-dependencies, `deb`, `rpm` and `apk` packager.
nFPM is a simple, 0-dependencies, `deb`, `rpm`, `apk`, and Arch Linux packager.
## Why