1
1
mirror of https://github.com/goreleaser/nfpm synced 2025-04-09 13:39:05 +02:00

feat: try to clean up and simplify the file adding interface (#255)

* feat: cleanup and simplify the file adding interface

* docs: update the configuration docs to focus on the new contents format for specifying files

* docs: correct spelling

Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Dj Gilcrease 2020-12-15 08:47:00 -08:00 committed by GitHub
parent 8535159b05
commit c4ae30d749
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 1015 additions and 637 deletions

5
.gitignore vendored

@ -1,5 +1,8 @@
vendor
*.rpm
*.deb
!dummy.deb
*.apk
coverage.txt
dist
nfpm.yaml
@ -8,3 +11,5 @@ bin
coverage.out
/nfpm
www/site
.idea/
testdata/acceptance/tmp/

@ -12,6 +12,7 @@ linters:
- gci
- exhaustivestruct
- wrapcheck
- godot
linters-settings:
maligned:
# print struct with more effective memory layout or not, false by default
@ -40,6 +41,8 @@ linters-settings:
- style
- performance
issues:
exclude:
- composites
exclude-rules:
- text: "G104" # gosec G104 is caught by errcheck
linters:

@ -2,6 +2,7 @@ SOURCE_FILES?=./...
TEST_PATTERN?=.
TEST_OPTIONS?=
TEST_TIMEOUT?=15m
TEST_PARALLEL?=2
export PATH := ./bin:$(PATH)
export GO111MODULE := on
@ -24,7 +25,7 @@ acceptance: pull_test_imgs
.PHONY: acceptance
test:
go test $(TEST_OPTIONS) -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=$(TEST_TIMEOUT)
go test $(TEST_OPTIONS) -p $(TEST_PARALLEL) -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=$(TEST_TIMEOUT)
.PHONY: test
cover: test

@ -1,5 +0,0 @@
// Package acceptance contains acceptance tests
package acceptance
// This file only exists to make go test happy.
// No code shall ever live in here.

@ -1,33 +0,0 @@
name: "foo"
arch: "386"
platform: "linux"
version: "v1.2.3-beta"
maintainer: "Foo Bar"
depends:
- bash
provides:
- fake
replaces:
- foo
suggests:
- zsh
description: |
Foo bar
Multiple lines
vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/folder/*: "/usr/share/whatever/folder/"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
empty_folders:
- /var/log/whatever
- /usr/share/foo
scripts:
preinstall: ./testdata/scripts/preinstall.sh
postinstall: ./testdata/scripts/postinstall.sh
preremove: ./testdata/scripts/preremove.sh
postremove: ./testdata/scripts/postremove.sh

@ -1,34 +0,0 @@
name: "foo"
arch: "amd64"
platform: "linux"
version: "v1.2.3-beta"
maintainer: "Foo Bar"
depends:
- bash
provides:
- fake
recommends:
- fish
replaces:
- foo
suggests:
- zsh
description: |
Foo bar
Multiple lines
vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/folder/*: "/usr/share/whatever/folder/"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
empty_folders:
- /var/log/whatever
- /usr/share/foo
scripts:
preinstall: ./testdata/scripts/preinstall.sh
postinstall: ./testdata/scripts/postinstall.sh
preremove: ./testdata/scripts/preremove.sh
postremove: ./testdata/scripts/postremove.sh

@ -1,32 +0,0 @@
name: "foo"
arch: "amd64"
platform: "linux"
version: "${SEMVER}"
maintainer: "Foo Bar"
depends:
- bash
provides:
- fake
replaces:
- foo
suggests:
- zsh
description: |
Foo bar
Multiple lines
vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/folder/**/*: "/usr/share/whatever/folder/"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
empty_folders:
- /var/log/whatever
- /usr/share/foo
scripts:
preinstall: ./testdata/scripts/preinstall.sh
postinstall: ./testdata/scripts/postinstall.sh
preremove: ./testdata/scripts/preremove.sh
postremove: ./testdata/scripts/postremove.sh

@ -1,37 +0,0 @@
name: "foo"
arch: "amd64"
platform: "linux"
version: "v1.2.3-beta"
maintainer: "Foo Bar"
depends:
- bash
provides:
- fake
replaces:
- foo
suggests:
- zsh
description: |
Foo bar
Multiple lines
vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/folder/*: "/usr/share/whatever/folder/"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
overrides:
rpm:
scripts:
preinstall: ./testdata/scripts/preinstall.sh
postremove: ./testdata/scripts/postremove.sh
deb:
scripts:
postinstall: ./testdata/scripts/postinstall.sh
preremove: ./testdata/scripts/preremove.sh
apk:
scripts:
postinstall: ./testdata/scripts/postinstall.sh
preremove: ./testdata/scripts/preremove.sh

@ -1,6 +1,6 @@
//+build acceptance
// +build acceptance
package acceptance
package nfpm_test
import (
"fmt"
@ -12,7 +12,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/goreleaser/nfpm"
// shut up
_ "github.com/goreleaser/nfpm/apk"
_ "github.com/goreleaser/nfpm/deb"
_ "github.com/goreleaser/nfpm/rpm"
@ -83,10 +82,10 @@ func TestComplex(t *testing.T) {
}
func TestConfigNoReplace(t *testing.T) {
var target = "./testdata/tmp/noreplace_old_rpm.rpm"
require.NoError(t, os.MkdirAll("./testdata/tmp", 0700))
var target = "./testdata/acceptance/tmp/noreplace_old_rpm.rpm"
require.NoError(t, os.MkdirAll("./testdata/acceptance/tmp", 0700))
config, err := nfpm.ParseFile("./testdata/config-noreplace-old.yaml")
config, err := nfpm.ParseFile("./testdata/acceptance/config-noreplace-old.yaml")
require.NoError(t, err)
info, err := config.Get("rpm")
@ -277,13 +276,13 @@ type testWriter struct {
}
func (t testWriter) Write(p []byte) (n int, err error) {
t.t.Logf(string(p))
t.t.Log(string(p))
return len(p), nil
}
func accept(t *testing.T, params acceptParms) {
var configFile = filepath.Join("./testdata", params.Conf)
tmp, err := filepath.Abs("./testdata/tmp")
var configFile = filepath.Join("./testdata/acceptance/", params.Conf)
tmp, err := filepath.Abs("./testdata/acceptance/tmp")
require.NoError(t, err)
var packageName = params.Name + "." + params.Format
var target = filepath.Join(tmp, packageName)
@ -312,7 +311,7 @@ func accept(t *testing.T, params acceptParms) {
"--build-arg", "package="+filepath.Join("tmp", packageName),
".",
)
cmd.Dir = "./testdata"
cmd.Dir = "./testdata/acceptance"
cmd.Stderr = testWriter{t}
cmd.Stdout = cmd.Stderr

@ -48,13 +48,15 @@ import (
"time"
"github.com/goreleaser/nfpm"
"github.com/goreleaser/nfpm/internal/files"
"github.com/goreleaser/nfpm/files"
"github.com/goreleaser/nfpm/internal/sign"
)
const packagerName = "apk"
// nolint: gochecknoinits
func init() {
nfpm.Register("apk", Default)
nfpm.RegisterPackager(packagerName, Default)
}
// nolint: gochecknoglobals
@ -106,6 +108,12 @@ func (*Apk) Package(info *nfpm.Info, apk io.Writer) (err error) {
if ok {
info.Arch = arch
}
if info.Arch == "" {
info.Arch = archToAlpine["amd64"]
}
if err = info.Validate(); err != nil {
return err
}
var bufData bytes.Buffer
@ -386,31 +394,44 @@ func createBuilderData(info *nfpm.Info, sizep *int64) func(tw *tar.Writer) error
return err
}
// handle Files and ConfigFiles
if err := createFilesInsideTarGz(info, tw, created, sizep); err != nil {
return err
}
return createSymlinksInsideTarGz(info, tw, created)
// handle Files
return createFilesInsideTarGz(info, tw, created, sizep)
}
}
func createFilesInsideTarGz(info *nfpm.Info, tw *tar.Writer, created map[string]bool, sizep *int64) error {
filesToCopy, err := files.Expand(info.Files, info.DisableGlobbing)
if err != nil {
return err
}
configFilesToCopy, err := files.Expand(info.ConfigFiles, info.DisableGlobbing)
if err != nil {
return err
}
for _, file := range append(filesToCopy, configFilesToCopy...) {
func createFilesInsideTarGz(info *nfpm.Info, tw *tar.Writer, created map[string]bool, sizep *int64) (err error) {
for _, file := range info.Contents {
if file.Packager != "" && file.Packager != packagerName {
continue
}
if err = createTree(tw, file.Destination, created); err != nil {
return err
}
err := copyToTarAndDigest(file.Source, file.Destination, tw, sizep, created)
switch file.Type {
case "ghost":
// skip ghost files in apk
continue
case "symlink":
err = createSymlinkInsideTarGz(file, tw)
case "doc":
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
case "licence", "license":
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
case "readme":
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
case "config", "config|noreplace":
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
default:
err = copyToTarAndDigest(file, tw, sizep, created)
}
if err != nil {
return err
}
@ -419,56 +440,37 @@ func createFilesInsideTarGz(info *nfpm.Info, tw *tar.Writer, created map[string]
return nil
}
func createSymlinksInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[string]bool) error {
for src, dst := range info.Symlinks {
if err := createTree(out, src, created); err != nil {
return err
}
err := newItemInsideTarGz(out, []byte{}, &tar.Header{
Name: strings.TrimLeft(src, "/"),
Linkname: dst,
Typeflag: tar.TypeSymlink,
ModTime: time.Now(),
Format: tar.FormatGNU,
})
if err != nil {
return err
}
}
return nil
func createSymlinkInsideTarGz(file *files.Content, out *tar.Writer) error {
return newItemInsideTarGz(out, []byte{}, &tar.Header{
Name: strings.TrimLeft(file.Destination, "/"),
Linkname: file.Source,
Typeflag: tar.TypeSymlink,
ModTime: file.FileInfo.MTime,
Format: tar.FormatGNU,
})
}
func copyToTarAndDigest(src, dst string, tw *tar.Writer, sizep *int64, created map[string]bool) error {
file, err := os.OpenFile(src, os.O_RDONLY, 0600) //nolint:gosec
func copyToTarAndDigest(file *files.Content, tw *tar.Writer, sizep *int64, created map[string]bool) error {
tarFile, err := os.OpenFile(file.Source, os.O_RDONLY, 0600) //nolint:gosec
if err != nil {
return fmt.Errorf("could not add file to the archive: %w", err)
}
// don't care if it errs while closing...
defer file.Close() // nolint: errcheck
info, err := file.Stat()
if err != nil {
return err
}
if info.IsDir() {
// TODO: this should probably return an error
return nil
}
header, err := tar.FileInfoHeader(info, src)
defer tarFile.Close() // nolint: errcheck
header, err := tar.FileInfoHeader(file, file.Source)
if err != nil {
log.Print(err)
return err
}
header.Name = files.ToNixPath(dst[1:])
err = writeFile(tw, header, file)
header.Name = files.ToNixPath(file.Destination[1:])
err = writeFile(tw, header, tarFile)
if err != nil {
return err
}
*sizep += info.Size()
created[src] = true
*sizep += file.Size()
created[file.Source] = true
return nil
}

@ -13,12 +13,11 @@ import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goreleaser/nfpm"
"github.com/goreleaser/nfpm/internal/sign"
"github.com/stretchr/testify/assert"
)
// nolint: gochecknoglobals
@ -77,7 +76,7 @@ func exampleInfo() *nfpm.Info {
}
func TestArchToAlpine(t *testing.T) {
verifyArch(t, "", "")
verifyArch(t, "", "x86_64")
verifyArch(t, "abc", "abc")
verifyArch(t, "386", "x86")
verifyArch(t, "amd64", "x86_64")
@ -97,6 +96,8 @@ func verifyArch(t *testing.T, nfpmArch, expectedArch string) {
func TestCreateBuilderData(t *testing.T) {
info := exampleInfo()
err := info.Validate()
require.NoError(t, err)
size := int64(0)
builderData := createBuilderData(info, &size)
@ -149,7 +150,7 @@ func TestDefaultWithArch(t *testing.T) {
func TestNoInfo(t *testing.T) {
var err = Default.Package(nfpm.WithDefaults(&nfpm.Info{}), ioutil.Discard)
assert.NoError(t, err)
assert.Error(t, err)
}
func TestFileDoesNotExist(t *testing.T) {
@ -207,6 +208,8 @@ func TestNoFiles(t *testing.T) {
func TestCreateBuilderControl(t *testing.T) {
info := exampleInfo()
size := int64(12345)
err := info.Validate()
require.NoError(t, err)
builderControl := createBuilderControl(info, size, sha256.New().Sum(nil))
var w bytes.Buffer
@ -231,6 +234,8 @@ func TestCreateBuilderControlScripts(t *testing.T) {
PreRemove: "../testdata/scripts/preremove.sh",
PostRemove: "../testdata/scripts/postremove.sh",
}
err := info.Validate()
require.NoError(t, err)
size := int64(12345)
builderControl := createBuilderControl(info, size, sha256.New().Sum(nil))
@ -269,6 +274,8 @@ func TestSignature(t *testing.T) {
info.APK.Signature.KeyFile = "../internal/sign/testdata/rsa.priv"
info.APK.Signature.KeyName = "testkey.rsa.pub"
info.APK.Signature.KeyPassphrase = "hunter2"
err := info.Validate()
require.NoError(t, err)
digest := sha1.New().Sum(nil) // nolint:gosec
@ -277,7 +284,7 @@ func TestSignature(t *testing.T) {
require.NoError(t, createSignatureBuilder(digest, info)(tw))
signature := extractFromTar(t, signatureTarGz.Bytes(), ".SIGN.RSA.testkey.rsa.pub")
err := sign.RSAVerifySHA1Digest(digest, signature, "../internal/sign/testdata/rsa.pub")
err = sign.RSAVerifySHA1Digest(digest, signature, "../internal/sign/testdata/rsa.pub")
require.NoError(t, err)
err = Default.Package(info, ioutil.Discard)
@ -289,13 +296,15 @@ func TestSignatureError(t *testing.T) {
info.APK.Signature.KeyFile = "../internal/sign/testdata/rsa.priv"
info.APK.Signature.KeyName = "testkey.rsa.pub"
info.APK.Signature.KeyPassphrase = "hunter2"
err := info.Validate()
require.NoError(t, err)
// wrong hash format
digest := sha256.New().Sum(nil)
var signatureTarGz bytes.Buffer
err := createSignature(&signatureTarGz, info, digest)
err = createSignature(&signatureTarGz, info, digest)
require.Error(t, err)
var expectedError *nfpm.ErrSigningFailure
@ -314,10 +323,12 @@ func TestDisableGlobbing(t *testing.T) {
info.Files = map[string]string{
"../testdata/{file}[": "/test/{file}[",
}
err := info.Validate()
require.NoError(t, err)
size := int64(0)
var dataTarGz bytes.Buffer
_, err := createData(&dataTarGz, info, &size)
_, err = createData(&dataTarGz, info, &size)
require.NoError(t, err)
gzr, err := gzip.NewReader(&dataTarGz)

@ -10,6 +10,7 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
@ -20,13 +21,15 @@ import (
"github.com/goreleaser/chglog"
"github.com/goreleaser/nfpm"
"github.com/goreleaser/nfpm/internal/files"
"github.com/goreleaser/nfpm/files"
"github.com/goreleaser/nfpm/internal/sign"
)
const packagerName = "deb"
// nolint: gochecknoinits
func init() {
nfpm.Register("deb", Default)
nfpm.RegisterPackager(packagerName, Default)
}
// nolint: gochecknoglobals
@ -83,6 +86,9 @@ func (*Deb) Package(info *nfpm.Info, deb io.Writer) (err error) { // nolint: fun
if ok {
info.Arch = arch
}
if err = info.Validate(); err != nil {
return err
}
dataTarGz, md5sums, instSize, err := createDataTarGz(info)
if err != nil {
@ -179,10 +185,6 @@ func createDataTarGz(info *nfpm.Info) (dataTarGz, md5sums []byte, instSize int64
return nil, nil, 0, err
}
if err := createSymlinksInsideTarGz(info, out, created); err != nil {
return nil, nil, 0, err
}
if err := out.Close(); err != nil {
return nil, nil, 0, fmt.Errorf("closing data.tar.gz: %w", err)
}
@ -193,50 +195,51 @@ func createDataTarGz(info *nfpm.Info) (dataTarGz, md5sums []byte, instSize int64
return buf.Bytes(), md5buf.Bytes(), instSize, nil
}
func createSymlinksInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[string]bool) error {
for src, dst := range info.Symlinks {
if err := createTree(out, src, created); err != nil {
return err
}
err := newItemInsideTarGz(out, []byte{}, &tar.Header{
Name: normalizePath(src),
Linkname: dst,
Typeflag: tar.TypeSymlink,
ModTime: time.Now(),
Format: tar.FormatGNU,
})
if err != nil {
return err
}
}
return nil
func createSymlinkInsideTarGz(file *files.Content, out *tar.Writer) error {
return newItemInsideTarGz(out, []byte{}, &tar.Header{
Name: normalizePath(file.Destination),
Linkname: file.Source,
Typeflag: tar.TypeSymlink,
ModTime: file.FileInfo.MTime,
Format: tar.FormatGNU,
})
}
func createFilesInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[string]bool) (bytes.Buffer, int64, error) {
var md5buf bytes.Buffer
var instSize int64
filesToCopy, err := files.Expand(info.Files, info.DisableGlobbing)
if err != nil {
return md5buf, 0, err
}
configFiles, err := files.Expand(info.ConfigFiles, info.DisableGlobbing)
if err != nil {
return md5buf, 0, err
}
filesToCopy = append(filesToCopy, configFiles...)
for _, file := range filesToCopy {
if err = createTree(out, file.Destination, created); err != nil {
func createFilesInsideTarGz(info *nfpm.Info, tw *tar.Writer, created map[string]bool) (md5buf bytes.Buffer, instSize int64, err error) {
for _, file := range info.Contents {
if file.Packager != "" && file.Packager != packagerName {
continue
}
if err = createTree(tw, file.Destination, created); err != nil {
return md5buf, 0, err
}
var size int64 // declare early to avoid shadowing err
size, err = copyToTarAndDigest(out, &md5buf, file.Source, file.Destination)
switch file.Type {
case "ghost":
// skip ghost files in apk
continue
case "symlink":
err = createSymlinkInsideTarGz(file, tw)
case "doc":
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
case "licence", "license":
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
case "readme":
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
case "config", "config|noreplace":
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
default:
size, err = copyToTarAndDigest(file, tw, &md5buf)
}
if err != nil {
return md5buf, 0, err
}
@ -244,7 +247,7 @@ func createFilesInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[string
}
if info.Changelog != "" {
size, err := createChangelogInsideTarGz(out, &md5buf, created, info)
size, err := createChangelogInsideTarGz(tw, &md5buf, created, info)
if err != nil {
return md5buf, 0, err
}
@ -267,43 +270,35 @@ func createEmptyFoldersInsideTarGz(info *nfpm.Info, out *tar.Writer, created map
return nil
}
func copyToTarAndDigest(tarw *tar.Writer, md5w io.Writer, src, dst string) (int64, error) {
file, err := os.OpenFile(src, os.O_RDONLY, 0600) //nolint:gosec
func copyToTarAndDigest(file *files.Content, tw *tar.Writer, md5w io.Writer) (int64, error) {
tarFile, err := os.OpenFile(file.Source, os.O_RDONLY, 0600) //nolint:gosec
if err != nil {
return 0, fmt.Errorf("could not add file to the archive: %w", err)
return 0, fmt.Errorf("could not add tarFile to the archive: %w", err)
}
// don't care if it errs while closing...
defer file.Close() // nolint: errcheck,gosec
info, err := file.Stat()
defer tarFile.Close() // nolint: errcheck,gosec
header, err := tar.FileInfoHeader(file, file.Source)
if err != nil {
log.Print(err)
return 0, err
}
if info.IsDir() {
// TODO: this should probably return an error
return 0, nil
}
var header = tar.Header{
Name: normalizePath(dst),
Size: info.Size(),
Mode: int64(info.Mode()),
ModTime: time.Now(),
Format: tar.FormatGNU,
}
if err := tarw.WriteHeader(&header); err != nil {
return 0, fmt.Errorf("cannot write header of %s to data.tar.gz: %w", src, err)
header.Format = tar.FormatGNU
header.Name = normalizePath(file.Destination)
if err := tw.WriteHeader(header); err != nil {
return 0, fmt.Errorf("cannot write header of %s to data.tar.gz: %w", file.Source, err)
}
var digest = md5.New() // nolint:gas
if _, err := io.Copy(tarw, io.TeeReader(file, digest)); err != nil {
if _, err := io.Copy(tw, io.TeeReader(tarFile, digest)); err != nil {
return 0, fmt.Errorf("failed to copy: %w", err)
}
if _, err := fmt.Fprintf(md5w, "%x %s\n", digest.Sum(nil), header.Name); err != nil {
return 0, fmt.Errorf("failed to write md5: %w", err)
}
return info.Size(), nil
return file.Size(), nil
}
func createChangelogInsideTarGz(tarw *tar.Writer, md5w io.Writer, created map[string]bool,
info *nfpm.Info) (int64, error) {
func createChangelogInsideTarGz(tarw *tar.Writer, md5w io.Writer, created map[string]bool, info *nfpm.Info) (int64, error) {
var buf bytes.Buffer
var out = gzip.NewWriter(&buf)
// the writers are properly closed later, this is just in case that we have
@ -551,8 +546,14 @@ func pathsToCreate(dst string) []string {
func conffiles(info *nfpm.Info) []byte {
// nolint: prealloc
var confs []string
for _, dst := range info.ConfigFiles {
confs = append(confs, dst)
for _, file := range info.Contents {
if file.Packager != "" && file.Packager != packagerName {
continue
}
switch file.Type {
case "config", "config|noreplace":
confs = append(confs, file.Destination)
}
}
return []byte(strings.Join(confs, "\n") + "\n")
}

@ -19,15 +19,13 @@ import (
"testing"
"github.com/blakesmith/ar"
"github.com/goreleaser/chglog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goreleaser/chglog"
"github.com/goreleaser/nfpm/internal/files"
"github.com/goreleaser/nfpm/internal/sign"
"github.com/goreleaser/nfpm"
"github.com/goreleaser/nfpm/files"
"github.com/goreleaser/nfpm/internal/sign"
)
// nolint: gochecknoglobals
@ -307,17 +305,26 @@ func TestDebNoFiles(t *testing.T) {
func TestDebNoInfo(t *testing.T) {
var err = Default.Package(nfpm.WithDefaults(&nfpm.Info{}), ioutil.Discard)
assert.NoError(t, err)
assert.Error(t, err)
}
func TestConffiles(t *testing.T) {
out := conffiles(&nfpm.Info{
info := nfpm.WithDefaults(&nfpm.Info{
Name: "minimal",
Arch: "arm64",
Description: "Minimal does nothing",
Priority: "extra",
Version: "1.0.0",
Section: "default",
Overridables: nfpm.Overridables{
ConfigFiles: map[string]string{
"fake": "/etc/fake",
"../testdata/fake": "/etc/fake",
},
},
})
err := info.Validate()
require.NoError(t, err)
out := conffiles(info)
assert.Equal(t, "/etc/fake\n", string(out), "should have a trailing empty line")
}
@ -471,6 +478,8 @@ func TestDebChangelogControl(t *testing.T) {
Version: "1.0.0",
Changelog: "../testdata/changelog.yaml",
}
err := info.Validate()
require.NoError(t, err)
controlTarGz, err := createControl(0, []byte{}, info)
require.NoError(t, err)
@ -491,6 +500,8 @@ func TestDebNoChangelogControlWithoutChangelogConfigured(t *testing.T) {
Description: "This package has explicitly no changelog.",
Version: "1.0.0",
}
err := info.Validate()
require.NoError(t, err)
controlTarGz, err := createControl(0, []byte{}, info)
require.NoError(t, err)
@ -507,6 +518,8 @@ func TestDebChangelogData(t *testing.T) {
Version: "1.0.0",
Changelog: "../testdata/changelog.yaml",
}
err := info.Validate()
require.NoError(t, err)
dataTarGz, _, _, err := createDataTarGz(info)
require.NoError(t, err)
@ -531,6 +544,8 @@ func TestDebNoChangelogDataWithoutChangelogConfigured(t *testing.T) {
Description: "This package has explicitly no changelog.",
Version: "1.0.0",
}
err := info.Validate()
require.NoError(t, err)
dataTarGz, _, _, err := createDataTarGz(info)
require.NoError(t, err)
@ -559,6 +574,8 @@ func TestDebTriggers(t *testing.T) {
},
},
}
err := info.Validate()
require.NoError(t, err)
controlTarGz, err := createControl(0, []byte{}, info)
require.NoError(t, err)
@ -597,6 +614,8 @@ func TestDebNoTriggersInControlIfNoneProvided(t *testing.T) {
Description: "This package has explicitly no triggers.",
Version: "1.0.0",
}
err := info.Validate()
require.NoError(t, err)
controlTarGz, err := createControl(0, []byte{}, info)
require.NoError(t, err)
@ -622,6 +641,8 @@ func TestSymlinkInFiles(t *testing.T) {
},
},
}
err := info.Validate()
require.NoError(t, err)
realSymlinkTarget, err := ioutil.ReadFile(symlinkTarget)
require.NoError(t, err)
@ -656,6 +677,8 @@ func TestSymlink(t *testing.T) {
},
},
}
err := info.Validate()
require.NoError(t, err)
dataTarGz, _, _, err := createDataTarGz(info)
require.NoError(t, err)
@ -674,6 +697,8 @@ func TestEnsureRelativePrefixInTarGzFiles(t *testing.T) {
"/symlink/to/fake.txt": "/usr/share/doc/fake/fake.txt",
}
info.Changelog = "../testdata/changelog.yaml"
err := info.Validate()
require.NoError(t, err)
dataTarGz, md5sums, instSize, err := createDataTarGz(info)
require.NoError(t, err)
@ -687,8 +712,15 @@ func TestEnsureRelativePrefixInTarGzFiles(t *testing.T) {
func TestMD5Sums(t *testing.T) {
info := exampleInfo()
info.Changelog = "../testdata/changelog.yaml"
err := info.Validate()
require.NoError(t, err)
nFiles := len(info.Files) + len(info.ConfigFiles) + 1 // +1 is the changelog
nFiles := 1
for _, f := range info.Contents {
if f.Packager == "" || f.Packager == "deb" {
nFiles++
}
}
dataTarGz, md5sums, instSize, err := createDataTarGz(info)
require.NoError(t, err)
@ -780,6 +812,8 @@ func TestDisableGlobbing(t *testing.T) {
info.Files = map[string]string{
"../testdata/{file}[": "/test/{file}[",
}
err := info.Validate()
require.NoError(t, err)
dataTarGz, _, _, err := createDataTarGz(info)
require.NoError(t, err)

197
files/files.go Normal file

@ -0,0 +1,197 @@
package files
import (
"fmt"
"os"
"path/filepath"
"sort"
"time"
"github.com/goreleaser/fileglob"
"gopkg.in/yaml.v3"
"github.com/goreleaser/nfpm/internal/glob"
)
// Content describes the source and destination
// of one file to copy into a package.
type Content struct {
Source string `yaml:"src,omitempty"`
Destination string `yaml:"dst,omitempty"`
Type string `yaml:"type,omitempty"`
Packager string `yaml:"packager,omitempty"`
FileInfo *ContentFileInfo `yaml:"file_info,omitempty"`
}
type ContentFileInfo struct {
Owner string `yaml:"owner,omitempty"`
Group string `yaml:"group"`
Mode os.FileMode `yaml:"mode,omitempty"`
MTime time.Time `yaml:"mtime,omitempty"`
Size int64 `yaml:"-"`
}
// Contents list of Content to process.
type Contents []*Content
type simpleContents map[string]string
func (c *Contents) UnmarshalYAML(value *yaml.Node) (err error) {
// nolint:exhaustive
// we do not care about `AliasNode`, `DocumentNode`
switch value.Kind {
case yaml.SequenceNode:
type tmpContents Contents
var tmp tmpContents
if err = value.Decode(&tmp); err != nil {
return err
}
*c = Contents(tmp)
case yaml.MappingNode:
var tmp simpleContents
if err = value.Decode(&tmp); err != nil {
return err
}
for src, dst := range tmp {
*c = append(*c, &Content{
Source: src,
Destination: dst,
})
}
case yaml.ScalarNode:
// TODO: implement issue-43 here and remove the fallthrough
// nolint:gocritic
// ignoring `emptyFallthrough: remove empty case containing only fallthrough to default case`
fallthrough
default:
// nolint:goerr113
// this is temporary so we do not need the a static error
return fmt.Errorf("not implemented")
}
return nil
}
func (c Contents) Len() int {
return len(c)
}
func (c Contents) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (c Contents) Less(i, j int) bool {
a, b := c[i], c[j]
if a.Type != b.Type {
return len(a.Type) < len(b.Type)
}
if a.Source != b.Source {
return a.Source < b.Source
}
return a.Destination < b.Destination
}
func (c *Content) WithFileInfoDefaults() {
if c.FileInfo == nil {
c.FileInfo = &ContentFileInfo{}
}
if c.FileInfo.Owner == "" {
c.FileInfo.Owner = "root"
}
if c.FileInfo.Group == "" {
c.FileInfo.Group = "root"
}
info, err := os.Stat(c.Source)
if err == nil {
c.FileInfo.MTime = info.ModTime()
c.FileInfo.Mode = info.Mode()
c.FileInfo.Size = info.Size()
}
if c.FileInfo.MTime.IsZero() {
c.FileInfo.MTime = time.Now().UTC()
}
}
// Name to part of the os.FileInfo interface
func (c *Content) Name() string {
return c.Source
}
// Size to part of the os.FileInfo interface
func (c *Content) Size() int64 {
return c.FileInfo.Size
}
// Mode to part of the os.FileInfo interface
func (c *Content) Mode() os.FileMode {
return c.FileInfo.Mode
}
// ModTime to part of the os.FileInfo interface
func (c *Content) ModTime() time.Time {
return c.FileInfo.MTime
}
// IsDir to part of the os.FileInfo interface
func (c *Content) IsDir() bool {
return false
}
// Sys to part of the os.FileInfo interface
func (c *Content) Sys() interface{} {
return nil
}
// ExpandContentGlobs gathers all of the real files to be copied into the package.
func ExpandContentGlobs(filesSrcDstMap Contents, disableGlobbing bool) (files Contents, err error) {
for _, f := range filesSrcDstMap {
var globbed map[string]string
if disableGlobbing {
f.Source = fileglob.QuoteMeta(f.Source)
}
switch f.Type {
case "ghost", "symlink":
// Ghost and symlink files need to be in the list, but dont glob them because they do not really exist
f.WithFileInfoDefaults()
files = append(files, f)
continue
}
globbed, err = glob.Glob(f.Source, f.Destination)
if err != nil {
return nil, err
}
files = appendGlobbedFiles(globbed, f, files)
}
// sort the files for reproducibility and general cleanliness
sort.Sort(files)
return files, nil
}
func appendGlobbedFiles(globbed map[string]string, origFile *Content, incFiles Contents) (files Contents) {
files = append(files, incFiles...)
for src, dst := range globbed {
newFile := &Content{
Destination: ToNixPath(dst),
Source: ToNixPath(src),
Type: origFile.Type,
FileInfo: origFile.FileInfo,
Packager: origFile.Packager,
}
newFile.WithFileInfoDefaults()
files = append(files, newFile)
}
return files
}
// ToNixPath converts the given path to a nix-style path.
//
// Windows-style path separators are considered escape
// characters by some libraries, which can cause issues.
func ToNixPath(path string) string {
return filepath.ToSlash(filepath.Clean(path))
}

71
files/files_test.go Normal file

@ -0,0 +1,71 @@
package files_test
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/goreleaser/nfpm/files"
)
type testStruct struct {
Contents files.Contents `yaml:"contents"`
}
func TestBasicDecode(t *testing.T) {
var config testStruct
dec := yaml.NewDecoder(strings.NewReader(`---
contents:
- src: a
dst: b
- src: a
dst: b
type: "config|noreplace"
file_info:
mode: 0644
packager: "rpm"
mtime: 2008-01-02T15:04:05Z
`))
dec.KnownFields(true)
err := dec.Decode(&config)
require.NoError(t, err)
assert.Len(t, config.Contents, 2)
for _, f := range config.Contents {
fmt.Printf("%+#v\n", f)
assert.Equal(t, f.Source, "a")
assert.Equal(t, f.Destination, "b")
}
}
func TestMapperDecode(t *testing.T) {
var config testStruct
dec := yaml.NewDecoder(strings.NewReader(`---
contents:
a: b
a2: b2
`))
dec.KnownFields(true)
err := dec.Decode(&config)
require.NoError(t, err)
assert.Len(t, config.Contents, 2)
for _, f := range config.Contents {
fmt.Printf("%+#v\n", f)
assert.Equal(t, f.Packager, "")
assert.Equal(t, f.Type, "")
}
}
func TestStringDecode(t *testing.T) {
var config testStruct
dec := yaml.NewDecoder(strings.NewReader(`---
contents: /path/to/a/tgz
`))
dec.KnownFields(true)
err := dec.Decode(&config)
require.Error(t, err)
assert.Equal(t, err.Error(), "not implemented")
}

4
go.mod

@ -41,7 +41,7 @@ require (
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 // indirect
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d // indirect
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2 // indirect
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 // indirect
)

@ -1,54 +0,0 @@
package files
import (
"path/filepath"
"sort"
"github.com/goreleaser/fileglob"
"github.com/goreleaser/nfpm/internal/glob"
)
// FileToCopy describes the source and destination
// of one file to copy into a package.
type FileToCopy struct {
Source string
Destination string
}
// Expand gathers all of the real files to be copied into the package.
func Expand(filesSrcDstMap map[string]string, disableGlobbing bool) ([]FileToCopy, error) {
var files []FileToCopy
for srcGlob, dstRoot := range filesSrcDstMap {
if disableGlobbing {
srcGlob = fileglob.QuoteMeta(srcGlob)
}
globbed, err := glob.Glob(srcGlob, dstRoot)
if err != nil {
return nil, err
}
for src, dst := range globbed {
files = append(files, FileToCopy{ToNixPath(src), ToNixPath(dst)})
}
}
// sort the files for reproducibility and general cleanliness
sort.Slice(files, func(i, j int) bool {
a, b := files[i], files[j]
if a.Source != b.Source {
return a.Source < b.Source
}
return a.Destination < b.Destination
})
return files, nil
}
// ToNixPath converts the given path to a nix-style path.
//
// Windows-style path separators are considered escape
// characters by some libraries, which can cause issues.
func ToNixPath(path string) string {
return filepath.ToSlash(filepath.Clean(path))
}

@ -1,56 +0,0 @@
package files
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goreleaser/nfpm"
)
func TestListFilesToCopy(t *testing.T) {
info := &nfpm.Info{
Overridables: nfpm.Overridables{
ConfigFiles: map[string]string{
"../../testdata/whatever.conf": "/whatever",
},
Files: map[string]string{
"../../testdata/scripts/*": "/test",
},
},
}
regularFiles, err := Expand(info.Files, info.DisableGlobbing)
require.NoError(t, err)
configFiles, err := Expand(info.ConfigFiles, info.DisableGlobbing)
require.NoError(t, err)
// all the input files described in the config in sorted order by source path
require.Equal(t, []FileToCopy{
{"../../testdata/scripts/postinstall.sh", "/test/postinstall.sh"},
{"../../testdata/scripts/postremove.sh", "/test/postremove.sh"},
{"../../testdata/scripts/preinstall.sh", "/test/preinstall.sh"},
{"../../testdata/scripts/preremove.sh", "/test/preremove.sh"},
}, regularFiles)
require.Equal(t, []FileToCopy{
{"../../testdata/whatever.conf", "/whatever"},
}, configFiles)
}
func TestListFilesToCopyWithAndWithoutGlobbing(t *testing.T) {
_, err := Expand(map[string]string{
"../../testdata/{file}*": "/test/{file}[",
}, false)
assert.EqualError(t, err, "glob failed: ../../testdata/{file}*: no matching files")
files, err := Expand(map[string]string{
"../../testdata/{file}[": "/test/{file}[",
}, true)
require.NoError(t, err)
assert.Equal(t, []FileToCopy{
{"../../testdata/{file}[", "/test/{file}["},
}, files)
}

163
nfpm.go

@ -13,7 +13,9 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/goreleaser/chglog"
"github.com/imdario/mergo"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"github.com/goreleaser/nfpm/files"
)
// nolint: gochecknoglobals
@ -22,11 +24,18 @@ var (
lock sync.Mutex
)
// Register a new packager for the given format.
func Register(format string, p Packager) {
// RegisterPackager a new packager for the given format.
func RegisterPackager(format string, p Packager) {
lock.Lock()
defer lock.Unlock()
packagers[format] = p
lock.Unlock()
}
// ClearPackagers clear all registered packagers, used for testing.
func ClearPackagers() {
lock.Lock()
defer lock.Unlock()
packagers = map[string]Packager{}
}
// ErrNoPackager happens when no packager is registered for the given format.
@ -50,7 +59,7 @@ func Get(format string) (Packager, error) {
// Parse decodes YAML data from an io.Reader into a configuration struct.
func Parse(in io.Reader) (config Config, err error) {
dec := yaml.NewDecoder(in)
dec.SetStrict(true)
dec.KnownFields(true)
if err = dec.Decode(&config); err != nil {
return
}
@ -78,6 +87,8 @@ func Parse(in io.Reader) (config Config, err error) {
config.APK.Signature.KeyPassphrase = apkPassphrase
}
WithDefaults(&config.Info)
return config, config.Validate()
}
@ -117,6 +128,10 @@ func (c *Config) Get(format string) (info *Info, err error) {
// no overrides
return info, nil
}
if !override.hasResetFiles {
override.resetFiles(format)
c.Contents = append(c.Contents, override.Contents...)
}
if err = mergo.Merge(&info.Overridables, override, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to merge overrides into info: %w", err)
}
@ -125,6 +140,9 @@ func (c *Config) Get(format string) (info *Info, err error) {
// Validate ensures that the config is well typed.
func (c *Config) Validate() error {
if err := Validate(&c.Info); err != nil {
return err
}
for format := range c.Overrides {
if _, err := Get(format); err != nil {
return err
@ -157,22 +175,99 @@ type Info struct {
Target string `yaml:"-"`
}
func (i *Info) Validate() error {
i.resetFiles("")
return Validate(i)
}
// GetChangeLog parses the provided changelog file.
func (i *Info) GetChangeLog() (log *chglog.PackageChangeLog, err error) {
// if the file does not exist chglog.Parse will just silently
// create an empty changelog but we should notify the user instead
if _, err = os.Stat(i.Changelog); os.IsNotExist(err) {
return nil, err
}
entries, err := chglog.Parse(i.Changelog)
if err != nil {
return nil, err
}
return &chglog.PackageChangeLog{
Name: i.Name,
Entries: entries,
}, nil
}
// Overridables contain the field which are overridable in a package.
type Overridables struct {
Replaces []string `yaml:"replaces,omitempty"`
Provides []string `yaml:"provides,omitempty"`
Depends []string `yaml:"depends,omitempty"`
Recommends []string `yaml:"recommends,omitempty"`
Suggests []string `yaml:"suggests,omitempty"`
Conflicts []string `yaml:"conflicts,omitempty"`
Files map[string]string `yaml:"files,omitempty"`
ConfigFiles map[string]string `yaml:"config_files,omitempty"`
Symlinks map[string]string `yaml:"symlinks,omitempty"`
EmptyFolders []string `yaml:"empty_folders,omitempty"`
Scripts Scripts `yaml:"scripts,omitempty"`
RPM RPM `yaml:"rpm,omitempty"`
Deb Deb `yaml:"deb,omitempty"`
APK APK `yaml:"apk,omitempty"`
Replaces []string `yaml:"replaces,omitempty"`
Provides []string `yaml:"provides,omitempty"`
Depends []string `yaml:"depends,omitempty"`
Recommends []string `yaml:"recommends,omitempty"`
Suggests []string `yaml:"suggests,omitempty"`
Conflicts []string `yaml:"conflicts,omitempty"`
Contents files.Contents `yaml:"contents,omitempty"`
Files map[string]string `yaml:"files,omitempty"`
ConfigFiles map[string]string `yaml:"config_files,omitempty"`
Symlinks map[string]string `yaml:"symlinks,omitempty"`
EmptyFolders []string `yaml:"empty_folders,omitempty"`
Scripts Scripts `yaml:"scripts,omitempty"`
RPM RPM `yaml:"rpm,omitempty"`
Deb Deb `yaml:"deb,omitempty"`
APK APK `yaml:"apk,omitempty"`
hasResetFiles bool
}
func (o *Overridables) resetFiles(packager string) {
if o.hasResetFiles {
return
}
for src, dst := range o.Files {
o.Contents = append(o.Contents, &files.Content{
Source: src,
Destination: dst,
Packager: packager,
})
}
o.Files = nil
for src, dst := range o.ConfigFiles {
o.Contents = append(o.Contents, &files.Content{
Source: src,
Destination: dst,
Type: "config",
Packager: packager,
})
}
o.ConfigFiles = nil
// Symlinks is backwards from other in the config file
for dst, src := range o.Symlinks {
o.Contents = append(o.Contents, &files.Content{
Source: src,
Destination: dst,
Type: "symlink",
Packager: packager,
})
}
o.Symlinks = nil
for src, dst := range o.RPM.ConfigNoReplaceFiles {
o.Contents = append(o.Contents, &files.Content{
Source: src,
Destination: dst,
Type: "config|noreplace",
Packager: "rpm",
})
}
o.RPM.ConfigNoReplaceFiles = nil
for _, dst := range o.RPM.GhostFiles {
o.Contents = append(o.Contents, &files.Content{
Destination: dst,
Type: "ghost",
Packager: "rpm",
})
}
o.hasResetFiles = true
}
// RPM is custom configs that are only available on RPM packages.
@ -257,7 +352,7 @@ func (e ErrFieldEmpty) Error() string {
}
// Validate the given Info and returns an error if it is invalid.
func Validate(info *Info) error {
func Validate(info *Info) (err error) {
if info.Name == "" {
return ErrFieldEmpty{"name"}
}
@ -279,7 +374,8 @@ func Validate(info *Info) error {
fmt.Fprintln(os.Stderr, "Warning: bindir is deprecated and will be removed in a future version")
}
return nil
info.Contents, err = files.ExpandContentGlobs(info.Contents, info.DisableGlobbing)
return err
}
// WithDefaults set some sane defaults into the given Info.
@ -290,6 +386,12 @@ func WithDefaults(info *Info) *Info {
if info.Description == "" {
info.Description = "no description given"
}
if info.Arch == "" {
info.Arch = "amd64"
}
if info.Version == "" {
info.Version = "v0.0.0-rc0"
}
// parse the version as a semver so we can properly split the parts
// and support proper ordering for both rpm and deb
@ -307,25 +409,6 @@ func WithDefaults(info *Info) *Info {
return info
}
// GetChangeLog parses the provided changelog file.
func (info *Info) GetChangeLog() (log *chglog.PackageChangeLog, err error) {
// if the file does not exist chglog.Parse will just silently
// create an empty changelog but we should notify the user instead
if _, err = os.Stat(info.Changelog); os.IsNotExist(err) {
return nil, err
}
entries, err := chglog.Parse(info.Changelog)
if err != nil {
return nil, err
}
return &chglog.PackageChangeLog{
Name: info.Name,
Entries: entries,
}, nil
}
// ErrSigningFailure is returned whenever something went wrong during
// the package signing process. The underlying error can be unwrapped
// and could be crypto-related or something that occurred while adding

@ -1,4 +1,4 @@
package nfpm
package nfpm_test
import (
"fmt"
@ -10,72 +10,74 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goreleaser/nfpm"
)
func TestRegister(t *testing.T) {
format := "TestRegister"
pkgr := &fakePackager{}
Register(format, pkgr)
got, err := Get(format)
nfpm.RegisterPackager(format, pkgr)
got, err := nfpm.Get(format)
require.NoError(t, err)
assert.Equal(t, pkgr, got)
}
func TestGet(t *testing.T) {
format := "TestGet"
got, err := Get(format)
got, err := nfpm.Get(format)
require.Error(t, err)
assert.EqualError(t, err, "no packager registered for the format "+format)
assert.Nil(t, got)
pkgr := &fakePackager{}
Register(format, pkgr)
got, err = Get(format)
nfpm.RegisterPackager(format, pkgr)
got, err = nfpm.Get(format)
require.NoError(t, err)
assert.Equal(t, pkgr, got)
}
func TestDefaultsVersion(t *testing.T) {
info := &Info{
info := &nfpm.Info{
Version: "v1.0.0",
}
info = WithDefaults(info)
info = nfpm.WithDefaults(info)
assert.NotEmpty(t, info.Platform)
assert.Equal(t, "1.0.0", info.Version)
assert.Equal(t, "", info.Release)
assert.Equal(t, "", info.Prerelease)
info = &Info{
info = &nfpm.Info{
Version: "v1.0.0-rc1",
}
info = WithDefaults(info)
info = nfpm.WithDefaults(info)
assert.Equal(t, "1.0.0", info.Version)
assert.Equal(t, "", info.Release)
assert.Equal(t, "rc1", info.Prerelease)
info = &Info{
info = &nfpm.Info{
Version: "v1.0.0-beta1",
}
info = WithDefaults(info)
info = nfpm.WithDefaults(info)
assert.Equal(t, "1.0.0", info.Version)
assert.Equal(t, "", info.Release)
assert.Equal(t, "beta1", info.Prerelease)
info = &Info{
info = &nfpm.Info{
Version: "v1.0.0-1",
Release: "2",
Prerelease: "beta1",
}
info = WithDefaults(info)
info = nfpm.WithDefaults(info)
assert.Equal(t, "1.0.0", info.Version)
assert.Equal(t, "2", info.Release)
assert.Equal(t, "beta1", info.Prerelease)
info = &Info{
info = &nfpm.Info{
Version: "v1.0.0-1+xdg2",
Release: "2",
Prerelease: "beta1",
}
info = WithDefaults(info)
info = nfpm.WithDefaults(info)
assert.Equal(t, "1.0.0", info.Version)
assert.Equal(t, "2", info.Release)
assert.Equal(t, "beta1", info.Prerelease)
@ -83,31 +85,31 @@ func TestDefaultsVersion(t *testing.T) {
}
func TestDefaults(t *testing.T) {
info := &Info{
info := &nfpm.Info{
Platform: "darwin",
Version: "2.4.1",
Description: "no description given",
}
got := WithDefaults(info)
got := nfpm.WithDefaults(info)
assert.Equal(t, info, got)
}
func TestValidate(t *testing.T) {
require.NoError(t, Validate(&Info{
require.NoError(t, nfpm.Validate(&nfpm.Info{
Name: "as",
Arch: "asd",
Version: "1.2.3",
Overridables: Overridables{
Overridables: nfpm.Overridables{
Files: map[string]string{
"asa": "asd",
},
},
}))
require.NoError(t, Validate(&Info{
require.NoError(t, nfpm.Validate(&nfpm.Info{
Name: "as",
Arch: "asd",
Version: "1.2.3",
Overridables: Overridables{
Overridables: nfpm.Overridables{
ConfigFiles: map[string]string{
"asa": "asd",
},
@ -116,7 +118,7 @@ func TestValidate(t *testing.T) {
}
func TestValidateError(t *testing.T) {
for err, info := range map[string]Info{
for err, info := range map[string]nfpm.Info{
"package name must be provided": {},
"package arch must be provided": {
Name: "fo",
@ -129,27 +131,52 @@ func TestValidateError(t *testing.T) {
err := err
info := info
t.Run(err, func(t *testing.T) {
require.EqualError(t, Validate(&info), err)
require.EqualError(t, nfpm.Validate(&info), err)
})
}
}
func TestParseFile(t *testing.T) {
packagers = map[string]Packager{}
_, err := ParseFile("./testdata/overrides.yaml")
nfpm.ClearPackagers()
_, err := nfpm.ParseFile("./testdata/overrides.yaml")
require.Error(t, err)
Register("deb", &fakePackager{})
Register("rpm", &fakePackager{})
Register("apk", &fakePackager{})
_, err = ParseFile("./testdata/overrides.yaml")
nfpm.RegisterPackager("deb", &fakePackager{})
nfpm.RegisterPackager("rpm", &fakePackager{})
nfpm.RegisterPackager("apk", &fakePackager{})
_, err = nfpm.ParseFile("./testdata/overrides.yaml")
require.NoError(t, err)
_, err = ParseFile("./testdata/doesnotexist.yaml")
_, err = nfpm.ParseFile("./testdata/doesnotexist.yaml")
require.Error(t, err)
config, err := ParseFile("./testdata/versionenv.yaml")
config, err := nfpm.ParseFile("./testdata/versionenv.yaml")
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("v%s", os.Getenv("GOROOT")), config.Version)
}
func TestParseEnhancedFile(t *testing.T) {
config, err := nfpm.ParseFile("./testdata/contents.yaml")
require.NoError(t, err)
assert.Equal(t, config.Name, "contents foo")
shouldFind := 5
if len(config.Contents) != shouldFind {
t.Errorf("should have had %d files but found %d", shouldFind, len(config.Contents))
for idx, f := range config.Contents {
fmt.Printf("%d => %+#v\n", idx, f)
}
}
}
func TestParseEnhancedNestedGlobFile(t *testing.T) {
config, err := nfpm.ParseFile("./testdata/contents_glob.yaml")
require.NoError(t, err)
shouldFind := 3
if len(config.Contents) != shouldFind {
t.Errorf("should have had %d files but found %d", shouldFind, len(config.Contents))
for idx, f := range config.Contents {
fmt.Printf("%d => %+#v\n", idx, f)
}
}
}
func TestOptionsFromEnvironment(t *testing.T) {
const (
globalPass = "hunter2"
@ -163,7 +190,7 @@ func TestOptionsFromEnvironment(t *testing.T) {
t.Run("version", func(t *testing.T) {
os.Clearenv()
os.Setenv("VERSION", version)
info, err := Parse(strings.NewReader("name: foo\nversion: $VERSION"))
info, err := nfpm.Parse(strings.NewReader("name: foo\nversion: $VERSION"))
require.NoError(t, err)
assert.Equal(t, version, info.Version)
})
@ -171,7 +198,7 @@ func TestOptionsFromEnvironment(t *testing.T) {
t.Run("release", func(t *testing.T) {
os.Clearenv()
os.Setenv("RELEASE", release)
info, err := Parse(strings.NewReader("name: foo\nrelease: $RELEASE"))
info, err := nfpm.Parse(strings.NewReader("name: foo\nrelease: $RELEASE"))
require.NoError(t, err)
assert.Equal(t, release, info.Release)
})
@ -179,7 +206,7 @@ func TestOptionsFromEnvironment(t *testing.T) {
t.Run("global passphrase", func(t *testing.T) {
os.Clearenv()
os.Setenv("NFPM_PASSPHRASE", globalPass)
info, err := Parse(strings.NewReader("name: foo"))
info, err := nfpm.Parse(strings.NewReader("name: foo"))
require.NoError(t, err)
assert.Equal(t, globalPass, info.Deb.Signature.KeyPassphrase)
assert.Equal(t, globalPass, info.RPM.Signature.KeyPassphrase)
@ -192,7 +219,7 @@ func TestOptionsFromEnvironment(t *testing.T) {
os.Setenv("NFPM_DEB_PASSPHRASE", debPass)
os.Setenv("NFPM_RPM_PASSPHRASE", rpmPass)
os.Setenv("NFPM_APK_PASSPHRASE", apkPass)
info, err := Parse(strings.NewReader("name: foo"))
info, err := nfpm.Parse(strings.NewReader("name: foo"))
require.NoError(t, err)
assert.Equal(t, debPass, info.Deb.Signature.KeyPassphrase)
assert.Equal(t, rpmPass, info.RPM.Signature.KeyPassphrase)
@ -202,7 +229,7 @@ func TestOptionsFromEnvironment(t *testing.T) {
func TestOverrides(t *testing.T) {
file := "./testdata/overrides.yaml"
config, err := ParseFile(file)
config, err := nfpm.ParseFile(file)
require.NoError(t, err)
assert.Equal(t, "foo", config.Name)
assert.Equal(t, "amd64", config.Arch)
@ -212,9 +239,17 @@ func TestOverrides(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, deb.Depends, "deb_depend")
assert.NotContains(t, deb.Depends, "rpm_depend")
assert.Contains(t, deb.ConfigFiles, "deb.conf")
assert.NotContains(t, deb.ConfigFiles, "rpm.conf")
assert.Contains(t, deb.ConfigFiles, "whatever.conf")
for _, f := range deb.Contents {
fmt.Printf("%+#v\n", f)
assert.True(t, f.Packager != "rpm")
assert.True(t, f.Packager != "apk")
if f.Packager == "deb" {
assert.Contains(t, f.Destination, "/deb")
}
if f.Packager == "" {
assert.True(t, f.Destination == "/etc/foo/whatever.conf")
}
}
assert.Equal(t, "amd64", deb.Arch)
// rpm overrides
@ -222,9 +257,17 @@ func TestOverrides(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, rpm.Depends, "rpm_depend")
assert.NotContains(t, rpm.Depends, "deb_depend")
assert.Contains(t, rpm.ConfigFiles, "rpm.conf")
assert.NotContains(t, rpm.ConfigFiles, "deb.conf")
assert.Contains(t, rpm.ConfigFiles, "whatever.conf")
for _, f := range rpm.Contents {
fmt.Printf("%+#v\n", f)
assert.True(t, f.Packager != "deb")
assert.True(t, f.Packager != "apk")
if f.Packager == "rpm" {
assert.Contains(t, f.Destination, "/rpm")
}
if f.Packager == "" {
assert.True(t, f.Destination == "/etc/foo/whatever.conf")
}
}
assert.Equal(t, "amd64", rpm.Arch)
// no overrides
@ -235,10 +278,10 @@ func TestOverrides(t *testing.T) {
type fakePackager struct{}
func (*fakePackager) ConventionalFileName(info *Info) string {
func (*fakePackager) ConventionalFileName(info *nfpm.Info) string {
return ""
}
func (*fakePackager) Package(info *Info, w io.Writer) error {
func (*fakePackager) Package(info *nfpm.Info, w io.Writer) error {
return nil
}

@ -15,7 +15,7 @@ import (
"github.com/google/rpmpack"
"github.com/sassoftware/go-rpmutils/cpio"
"github.com/goreleaser/nfpm/internal/files"
"github.com/goreleaser/nfpm/files"
"github.com/goreleaser/nfpm/internal/sign"
"github.com/goreleaser/chglog"
@ -39,9 +39,11 @@ const (
{{- end}}{{- end}}`
)
const packagerName = "rpm"
// nolint: gochecknoinits
func init() {
nfpm.Register("rpm", Default)
nfpm.RegisterPackager(packagerName, Default)
}
// Default RPM packager.
@ -83,13 +85,15 @@ func (*RPM) ConventionalFileName(info *nfpm.Info) string {
}
// Package writes a new RPM package to the given writer using the given info.
func (*RPM) Package(info *nfpm.Info, w io.Writer) error {
func (*RPM) Package(info *nfpm.Info, w io.Writer) (err error) {
var (
err error
meta *rpmpack.RPMMetaData
rpm *rpmpack.RPM
)
info = ensureValidArch(info)
if err = info.Validate(); err != nil {
return err
}
if meta, err = buildRPMMeta(info); err != nil {
return err
@ -107,8 +111,6 @@ func (*RPM) Package(info *nfpm.Info, w io.Writer) error {
return err
}
addSymlinksInsideRPM(info, rpm)
if err = addScriptFiles(info, rpm); err != nil {
return err
}
@ -313,93 +315,73 @@ func addEmptyDirsRPM(info *nfpm.Info, rpm *rpmpack.RPM) {
}
}
func createFilesInsideRPM(info *nfpm.Info, rpm *rpmpack.RPM) error {
regularFiles, err := files.Expand(info.Files, info.DisableGlobbing)
if err != nil {
return err
}
func createFilesInsideRPM(info *nfpm.Info, rpm *rpmpack.RPM) (err error) {
var symlinks files.Contents
for _, file := range info.Contents {
if file.Packager != "" && file.Packager != packagerName {
continue
}
for _, file := range regularFiles {
err = copyToRPM(rpm, file.Source, file.Destination, rpmpack.GenericFile)
if err != nil {
var rpmFileType rpmpack.FileType
switch file.Type {
case "config":
rpmFileType = rpmpack.ConfigFile
case "config|noreplace":
rpmFileType = rpmpack.ConfigFile | rpmpack.NoReplaceFile
case "ghost":
rpmFileType = rpmpack.GhostFile
if file.FileInfo.Mode == 0 {
file.FileInfo.Mode = os.FileMode(0644)
}
case "doc":
rpmFileType = rpmpack.DocFile
case "licence", "license":
rpmFileType = rpmpack.LicenceFile
case "readme":
rpmFileType = rpmpack.ReadmeFile
case "symlink":
symlinks = append(symlinks, file)
continue
default:
rpmFileType = rpmpack.GenericFile
}
if err = copyToRPM(rpm, file, rpmFileType); err != nil {
return err
}
}
configFiles, err := files.Expand(info.ConfigFiles, info.DisableGlobbing)
if err != nil {
return err
}
for _, file := range configFiles {
err = copyToRPM(rpm, file.Source, file.Destination, rpmpack.ConfigFile)
if err != nil {
return err
}
}
configNoReplaceFiles, err := files.Expand(info.RPM.ConfigNoReplaceFiles, info.DisableGlobbing)
if err != nil {
return err
}
for _, file := range configNoReplaceFiles {
err = copyToRPM(rpm, file.Source, file.Destination, rpmpack.ConfigFile|rpmpack.NoReplaceFile)
if err != nil {
return err
}
}
// note: the ghost files will be created as empty files when the package is installed, which is not
// correct: https://github.com/google/rpmpack/issues/51
for _, destName := range info.RPM.GhostFiles {
rpm.AddFile(rpmpack.RPMFile{
Name: destName,
Mode: 0644,
MTime: uint32(time.Now().UTC().Unix()),
Owner: "root",
Group: "root",
Type: rpmpack.GhostFile,
})
}
addSymlinksInsideRPM(symlinks, rpm)
return nil
}
func addSymlinksInsideRPM(info *nfpm.Info, rpm *rpmpack.RPM) {
for src, dst := range info.Symlinks {
func addSymlinksInsideRPM(symlinks files.Contents, rpm *rpmpack.RPM) {
for _, file := range symlinks {
rpm.AddFile(rpmpack.RPMFile{
Name: src,
Body: []byte(dst),
Name: file.Destination,
Body: []byte(file.Source),
Mode: uint(cpio.S_ISLNK),
MTime: uint32(time.Now().UTC().Unix()),
Owner: "root",
Group: "root",
MTime: uint32(file.FileInfo.MTime.Unix()),
Owner: file.FileInfo.Owner,
Group: file.FileInfo.Group,
})
}
}
func copyToRPM(rpm *rpmpack.RPM, src, dst string, fileType rpmpack.FileType) error {
info, err := os.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
// TODO: this should probably return an error
return nil
}
data, err := ioutil.ReadFile(src)
if err != nil {
func copyToRPM(rpm *rpmpack.RPM, file *files.Content, fileType rpmpack.FileType) (err error) {
data, err := ioutil.ReadFile(file.Source)
if err != nil && file.Type != "ghost" {
return err
}
rpm.AddFile(rpmpack.RPMFile{
Name: dst,
Name: file.Destination,
Body: data,
Mode: uint(info.Mode()),
MTime: uint32(info.ModTime().Unix()),
Owner: "root",
Group: "root",
Mode: uint(file.FileInfo.Mode),
MTime: uint32(file.FileInfo.MTime.Unix()),
Owner: file.FileInfo.Owner,
Group: file.FileInfo.Group,
Type: fileType,
})

@ -13,16 +13,15 @@ import (
"testing"
"time"
"github.com/goreleaser/chglog"
"github.com/sassoftware/go-rpmutils"
"github.com/sassoftware/go-rpmutils/cpio"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/openpgp"
"github.com/goreleaser/chglog"
"github.com/goreleaser/nfpm"
"github.com/goreleaser/nfpm/internal/files"
"github.com/goreleaser/nfpm/files"
)
func exampleInfo() *nfpm.Info {

33
testdata/acceptance/complex.386.yaml vendored Normal file

@ -0,0 +1,33 @@
name: "foo"
arch: "386"
platform: "linux"
version: "v1.2.3-beta"
maintainer: "Foo Bar"
depends:
- bash
provides:
- fake
replaces:
- foo
suggests:
- zsh
description: |
Foo bar
Multiple lines
vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
./testdata/fake: "/usr/local/bin/fake"
./testdata/acceptance/folder/*: "/usr/share/whatever/folder/"
config_files:
./testdata/whatever.conf: "/etc/foo/whatever.conf"
empty_folders:
- /var/log/whatever
- /usr/share/foo
scripts:
preinstall: ./testdata/acceptance/scripts/preinstall.sh
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh
postremove: ./testdata/acceptance/scripts/postremove.sh

34
testdata/acceptance/complex.yaml vendored Normal file

@ -0,0 +1,34 @@
name: "foo"
arch: "amd64"
platform: "linux"
version: "v1.2.3-beta"
maintainer: "Foo Bar"
depends:
- bash
provides:
- fake
recommends:
- fish
replaces:
- foo
suggests:
- zsh
description: |
Foo bar
Multiple lines
vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
./testdata/fake: "/usr/local/bin/fake"
./testdata/acceptance/folder/*: "/usr/share/whatever/folder/"
config_files:
./testdata/whatever.conf: "/etc/foo/whatever.conf"
empty_folders:
- /var/log/whatever
- /usr/share/foo
scripts:
preinstall: ./testdata/acceptance/scripts/preinstall.sh
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh
postremove: ./testdata/acceptance/scripts/postremove.sh

@ -7,7 +7,7 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
config_files:
../testdata/whatever.conf: "/etc/regular.conf"
./testdata/whatever.conf: "/etc/regular.conf"
rpm:
config_noreplace_files:
../testdata/whatever.conf: "/etc/noreplace.conf"
./testdata/whatever.conf: "/etc/noreplace.conf"

@ -7,7 +7,7 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
config_files:
../testdata/whatever2.conf: "/etc/regular.conf"
./testdata/whatever2.conf: "/etc/regular.conf"
rpm:
config_noreplace_files:
../testdata/whatever2.conf: "/etc/noreplace.conf"
./testdata/whatever2.conf: "/etc/noreplace.conf"

@ -0,0 +1,32 @@
name: "foo"
arch: "amd64"
platform: "linux"
version: "${SEMVER}"
maintainer: "Foo Bar"
depends:
- bash
provides:
- fake
replaces:
- foo
suggests:
- zsh
description: |
Foo bar
Multiple lines
vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
./testdata/fake: "/usr/local/bin/fake"
./testdata/acceptance/folder/**/*: "/usr/share/whatever/folder/"
config_files:
./testdata/whatever.conf: "/etc/foo/whatever.conf"
empty_folders:
- /var/log/whatever
- /usr/share/foo
scripts:
preinstall: ./testdata/acceptance/scripts/preinstall.sh
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh
postremove: ./testdata/acceptance/scripts/postremove.sh

@ -10,8 +10,8 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
./testdata/whatever.conf: "/etc/foo/whatever.conf"
rpm:
compression: "gzip"

@ -10,8 +10,8 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
./testdata/whatever.conf: "/etc/foo/whatever.conf"
rpm:
compression: "lzma"

@ -3,4 +3,4 @@ arch: amd64
version: 1.2.3
license: MIT
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"

37
testdata/acceptance/overrides.yaml vendored Normal file

@ -0,0 +1,37 @@
name: "foo"
arch: "amd64"
platform: "linux"
version: "v1.2.3-beta"
maintainer: "Foo Bar"
depends:
- bash
provides:
- fake
replaces:
- foo
suggests:
- zsh
description: |
Foo bar
Multiple lines
vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
./testdata/fake: "/usr/local/bin/fake"
./testdata/acceptance/folder/*: "/usr/share/whatever/folder/"
config_files:
./testdata/whatever.conf: "/etc/foo/whatever.conf"
overrides:
rpm:
scripts:
preinstall: ./testdata/acceptance/scripts/preinstall.sh
postremove: ./testdata/acceptance/scripts/postremove.sh
deb:
scripts:
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh
apk:
scripts:
postinstall: ./testdata/acceptance/scripts/postinstall.sh
preremove: ./testdata/acceptance/scripts/preremove.sh

@ -11,6 +11,6 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
./testdata/whatever.conf: "/etc/foo/whatever.conf"

@ -10,9 +10,9 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
./testdata/whatever.conf: "/etc/foo/whatever.conf"
deb:
scripts:
rules: ./testdata/scripts/rules.sh
rules: ./testdata/acceptance/scripts/rules.sh

@ -7,13 +7,13 @@ description: This package is sigend
vendor: "FooBarCorp"
homepage: "http://example.com"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"
deb:
signature:
key_file: ../internal/sign/testdata/privkey_unprotected.asc
key_file: ./internal/sign/testdata/privkey_unprotected.asc
rpm:
signature:
key_file: ../internal/sign/testdata/privkey_unprotected.asc
key_file: ./internal/sign/testdata/privkey_unprotected.asc
apk:
signature:
key_file: ../internal/sign/testdata/rsa_unprotected.priv
key_file: ./internal/sign/testdata/rsa_unprotected.priv

@ -10,6 +10,6 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
./testdata/whatever.conf: "/etc/foo/whatever.conf"

@ -10,6 +10,6 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
./testdata/whatever.conf: "/etc/foo/whatever.conf"

@ -10,6 +10,6 @@ vendor: "foobar"
homepage: "https://foobar.org"
license: "MIT"
files:
../testdata/fake: "/usr/local/bin/fake"
./testdata/fake: "/usr/local/bin/fake"
config_files:
../testdata/whatever.conf: "/etc/foo/whatever.conf"
./testdata/whatever.conf: "/etc/foo/whatever.conf"

Some files were not shown because too many files have changed in this diff Show More