Repository: goreleaser/nfpm
Pull request #255 (2020-11-15 20:14:03)

feat: try to clean up and simplify the file adding interface
The goal is to make this backward compatible with current config files so people can stay with the simplified format until they need more advanced features. 

Internally all packagers `ONLY` use the Contents data which is a merging of all the old file specifications into one list. IF a file was specified in a packager specific override (eg `rpm.Files`) then the `content.Packager` field is set to the correct packager automatically which ensures files for other packagers do not get mixed together.

Example
```yaml
name: "contents foo"
arch: "amd64"
version: "v1.2.3"
contents:
  - src: "./testdata/*.conf"
    dst: "/etc/foo/"
    type: "config|noreplace"
    file_info:
      mode: 0644
      mtime: 2008-01-02T15:04:05Z
      owner: me
      group: whatever
  - src: "./testdata/whatever2.conf"
    dst: "/etc/rpm/whatever.conf"
    type: "config|noreplace"
    packager: "rpm"
    file_info:
      mode: 0644
  - src: "./testdata/whatever2.conf"
    dst: "/etc/apk/whatever.conf"
    type: "config|noreplace"
    packager: "apk"
    file_info:
      mode: 0644
  - src: "./testdata/whatever2.conf"
    dst: "/etc/deb/whatever.conf"
    type: "config|noreplace"
    packager: "deb"
    file_info:
      mode: 0644
```

The `files`, `config_files`, `symlinks`, `rpm.config_noreplace_files`, and `rpm.ghost_files` are merged into `contents` and all packagers only the the data from `contents` now. This makes it simple for people to leave their config files as is and not break anything, but as they need more advanced features, like control over the file mode, owner, group, etc they can convert all their files to the contents as well.

I have also set it up so the simple mapping will work as well
```yaml
contents:
  a: b
  a2: b2
```

Also this is setup to be ready to handle contents as a string in prep for #43 but is not implemented
```yaml
contents: /path/to/a/tgz
```

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/goreleaser/nfpm/rdojaljiw](https://vercel.com/goreleaser/nfpm/rdojaljiw)  
✅ Preview: [https://nfpm-git-feat-file-type-enhancement.goreleaser.vercel.app](https://nfpm-git-feat-file-type-enhancement.goreleaser.vercel.app)


erikgeiser: This is a pretty significant change and should definitely be discussed. I do think your approach is cleaner and more elegant but the old one is probably more pragmatic, since most people probably have a whole bunch of regular files and rarely any special files. I have no data to back that up though. I generate my configs automatically as a part of a bigger custom build process, but for those writing configs by hand, it will be much more to type.

I'll have a look at the exact changes when I find some time, maybe tomorrow.
djgilcrease: Ya, that's why I am making sure to ensure backwards compatibility so most people can continue to use the simple config format if they want. 
erikgeiser: If I read your code correctly you planned to implement a custom unmarshal method so `files` can be both a YAML mapping and a list of `FileToCopy` in the YAML file. This is a great way to implement it.

Maybe we can use `src` and `dst` as YAML keys to make it more convenient for regular files in the new format.

Also I dislike the name of the type `FileToCopy`. If it's a glob, it represents multiple files that need to be copied. If it's a ghost file, nothing is to be copied. Maybe we could rename it to `Content`.
erikgeiser: This is also nice for #43 because now we can then completely decide what do based on the YAML node type:
* **Mapping:** `src`->`dst` mapping of regular files, no non-regular files supported (only via depecated options), this is nice for very simple packages
* **Sequence:** The new rich file types you introduced here
* **String:** Path to a `.tar.gz` file

I think then `Content` or `Contents` would indeed be a much more fitting name.
djgilcrease: If we want a new field instead of trying to override the current `files` field that should actually be easier and cleaner than what I am having to do now I think. I can still have code that merges the `files`, `config_files`, `symlinks`, `rpm.config_noreplace_files`, and `rpm.ghost_files` into `contents` so all the packagers only use the `contents`

```yaml
contents:
  - src: "something"
    dst: "/opt/pkg/something"
    type: "config"
    packager: "rpm"
    mode: 0644
```

give me a day or so to play with that concept to see if it turns out cleaner
djgilcrease: > give me a day or so to play with that concept to see if it turns out cleaner

Much cleaner code now that I am not trying to mess with existing fields

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/nfpm/pull/255?src=pr&el=h1) Report
> Merging [#255](https://codecov.io/gh/goreleaser/nfpm/pull/255?src=pr&el=desc) (c858c29) into [master](https://codecov.io/gh/goreleaser/nfpm/commit/8535159b054d6c6ec19605e67afbf3516e2e4803?el=desc) (8535159) will **increase** coverage by `1.58%`.
> The diff coverage is `80.93%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/nfpm/pull/255/graphs/tree.svg?width=650&height=150&src=pr&token=9QKGIxn7wg)](https://codecov.io/gh/goreleaser/nfpm/pull/255?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #255      +/-   ##
==========================================
+ Coverage   68.79%   70.38%   +1.58%     
==========================================
  Files           9        9              
  Lines        1032     1138     +106     
==========================================
+ Hits          710      801      +91     
- Misses        190      209      +19     
+ Partials      132      128       -4     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/nfpm/pull/255?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [deb/deb.go](https://codecov.io/gh/goreleaser/nfpm/pull/255/diff?src=pr&el=tree#diff-ZGViL2RlYi5nbw==) | `66.22% <60.00%> (-2.35%)` | :arrow_down: |
| [apk/apk.go](https://codecov.io/gh/goreleaser/nfpm/pull/255/diff?src=pr&el=tree#diff-YXBrL2Fway5nbw==) | `70.63% <72.97%> (-1.47%)` | :arrow_down: |
| [rpm/rpm.go](https://codecov.io/gh/goreleaser/nfpm/pull/255/diff?src=pr&el=tree#diff-cnBtL3JwbS5nbw==) | `73.86% <75.60%> (+0.47%)` | :arrow_up: |
| [nfpm.go](https://codecov.io/gh/goreleaser/nfpm/pull/255/diff?src=pr&el=tree#diff-bmZwbS5nbw==) | `84.96% <90.47%> (+6.91%)` | :arrow_up: |
| [files/files.go](https://codecov.io/gh/goreleaser/nfpm/pull/255/diff?src=pr&el=tree#diff-ZmlsZXMvZmlsZXMuZ28=) | `92.95% <92.95%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/nfpm/pull/255?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/nfpm/pull/255?src=pr&el=footer). Last update [8535159...c858c29](https://codecov.io/gh/goreleaser/nfpm/pull/255?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

caarlos0: > The goal is to make this backward compatible with current config files so people can stay with the simplified format until they need more advanced features.

I would say we deprecate the old format. The new format is simple enough for the basic use case as well, and then when users need to change it, its not that much of a change?

wdyt?

if you agree, we can copy the deprecate package from goreleaser and do basically the same thing here, and remove the old option after 6mo or so. (and we can make this all in another PR since this one is already pretty big)

Patch:
diff --git a/.gitignore b/.gitignore
index 3b01d08..08ba215 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
\ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
index 2ad6e55..c6bc339 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -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:
diff --git a/Makefile b/Makefile
index ce63cee..17ea961 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/acceptance/placeholder.go b/acceptance/placeholder.go
deleted file mode 100644
index 4343761..0000000
--- a/acceptance/placeholder.go
+++ /dev/null
@@ -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.
diff --git a/apk/apk.go b/apk/apk.go
index 228ecc4..43b755b 100644
--- a/apk/apk.go
+++ b/apk/apk.go
@@ -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
 }
 
diff --git a/deb/deb.go b/deb/deb.go
index df689ba..5fff431 100644
--- a/deb/deb.go
+++ b/deb/deb.go
@@ -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")
 }
diff --git a/files/files.go b/files/files.go
new file mode 100644
index 0000000..7655a54
--- /dev/null
+++ b/files/files.go
@@ -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))
+}
diff --git a/go.mod b/go.mod
index f9fefb9..9f00832 100644
--- a/go.mod
+++ b/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
 )
diff --git a/internal/files/files.go b/internal/files/files.go
deleted file mode 100644
index b3d8f58..0000000
--- a/internal/files/files.go
+++ /dev/null
@@ -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))
-}
diff --git a/nfpm.go b/nfpm.go
index e11b494..1c17e95 100644
--- a/nfpm.go
+++ b/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
diff --git a/rpm/rpm.go b/rpm/rpm.go
index 67b748c..fa694de 100644
--- a/rpm/rpm.go
+++ b/rpm/rpm.go
@@ -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
-	}
-
-	for _, file := range regularFiles {
-		err = copyToRPM(rpm, file.Source, file.Destination, rpmpack.GenericFile)
-		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
 		}
-	}
-
-	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
+		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
 		}
-	}
-
-	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 {
+		if err = copyToRPM(rpm, file, rpmFileType); 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,
 	})
 
diff --git a/testdata/globtest/a.txt b/testdata/globtest/a.txt
new file mode 100644
index 0000000..e69de29
diff --git a/testdata/globtest/multi-nested/subdir/c.txt b/testdata/globtest/multi-nested/subdir/c.txt
new file mode 100644
index 0000000..e69de29
diff --git a/testdata/globtest/nested/b.txt b/testdata/globtest/nested/b.txt
new file mode 100644
index 0000000..e69de29
diff --git a/www/docs/configuration.md b/www/docs/configuration.md
index e9238da..e9bd53e 100644
--- a/www/docs/configuration.md
+++ b/www/docs/configuration.md
@@ -85,31 +85,75 @@ suggests:
 # Packages it conflicts with. (overridable)
 conflicts:
   - mercurial
-
-# Files to add to the package. (overridable)
+    
+# Contents to add to the package
 # This can be binaries or any other files.
-#
-# Key is the local file/glob, value is the path inside the package.
-files:
-  path/to/local/foo: /usr/local/bin/foo
-  some/glob/**/*: /usr/share/foo/
-
-# Config files are dealt with differently when upgrading and uninstalling the
-# package. (overridable)
-#
-# Key is the local file/glob, value is the path inside the package
-config_files:
-  path/to/local/foo.conf: /etc/foo.conf
-  some/glob/**/*: /etc/foo/
+contents:
+    # Basic file that applies to all packagers
+  - src: path/to/local/foo
+    dst: /usr/local/bin/foo
+    
+    # Simple config file
+  - src: path/to/local/foo.conf
+    dst: /etc/foo.conf
+    type: config
+    
+    # Simple symlink
+  - src: /sbin/foo # link name
+    dst: /usr/local/bin/foo # real location
+    type: "symlink"
+    
+    # Corresponds to %config(noreplace) if the packager is rpm, otherwise it is just a config file
+  - src: path/to/local/bar.conf
+    dst: /etc/bar.conf
+    type: "config|noreplace"
+
+    # These files are not actually present in the package, but the file names
+    # are added to the package header. From the RPM directives documentation:
+    #
+    # "There are times when a file should be owned by the package but not
+    # installed - log files and state files are good examples of cases you might
+    # desire this to happen."
+    #
+    # "The way to achieve this, is to use the %ghost directive. By adding this
+    # directive to the line containing a file, RPM will know about the ghosted
+    # file, but will not add it to the package."
+    # 
+    # For non rpm packages ghost files are ignored at this time.
+  - dst: /etc/casper.conf
+    type: ghost
+  - dst: /var/log/boo.log
+    type: ghost
+    
+    # You can user the packager field to add files that are unique to a specific packager 
+  - src: path/to/rpm/file.conf
+    dst: /etc/file.conf
+    type: "config|noreplace"
+    packager: rpm
+  - src: path/to/deb/file.conf
+    dst: /etc/file.conf
+    type: "config|noreplace"
+    packager: deb
+  - src: path/to/apk/file.conf
+    dst: /etc/file.conf
+    type: "config|noreplace"
+    packager: apk
+    
+    # Sometimes it is important to be able to set the mtime, mode, owner, or group for a file 
+    # that differs from what is on the local build system at build time.
+  - src: path/to/foo
+    dst: /usr/local/foo
+    file_info:
+      mode: 0644
+      mtime: 2008-01-02T15:04:05Z
+      owner: notRoot
+      group: notRoot
+
 
 # Empty folders your package may need created. (overridable)
 empty_folders:
   - /var/log/foo
 
-# Symlinks mapping from symlink name inside package to target inside package (overridable)
-symlinks:
-  /sbin/foo: /usr/local/bin/foo
-
 # Scripts to run at specific stages. (overridable)
 scripts:
   preinstall: ./scripts/preinstall.sh
@@ -147,25 +191,6 @@ rpm:
   # Compression algorithm.
   compression: lzma
 
-  # These config files will not be replaced by new versions if they were
-  # changed by the user. Corresponds to %config(noreplace).
-  config_noreplace_files:
-    path/to/local/bar.con: /etc/bar.conf
-
-  # These files are not actually present in the package, but the file names
-  # are added to the package header. From the RPM directives documentation:
-  #
-  # "There are times when a file should be owned by the package but not
-  # installed - log files and state files are good examples of cases you might
-  # desire this to happen."
-  #
-  # "The way to achieve this, is to use the %ghost directive. By adding this
-  # directive to the line containing a file, RPM will know about the ghosted
-  # file, but will not add it to the package."
-  ghost_files:
-    - /etc/casper.conf
-    - /var/log/boo.log
-
   # The package is signed if a key_file is set
   signature:
     # PGP secret key (can also be ASCII-armored), the passphrase is taken


Test patch:
diff --git a/acceptance/testdata/complex.386.yaml b/acceptance/testdata/complex.386.yaml
deleted file mode 100644
index d6dc028..0000000
--- a/acceptance/testdata/complex.386.yaml
+++ /dev/null
@@ -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
-
diff --git a/acceptance/testdata/complex.yaml b/acceptance/testdata/complex.yaml
deleted file mode 100644
index 8659bde..0000000
--- a/acceptance/testdata/complex.yaml
+++ /dev/null
@@ -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
diff --git a/acceptance/testdata/env-var-version.yaml b/acceptance/testdata/env-var-version.yaml
deleted file mode 100644
index 340bad4..0000000
--- a/acceptance/testdata/env-var-version.yaml
+++ /dev/null
@@ -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
diff --git a/acceptance/testdata/overrides.yaml b/acceptance/testdata/overrides.yaml
deleted file mode 100644
index c519a51..0000000
--- a/acceptance/testdata/overrides.yaml
+++ /dev/null
@@ -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
diff --git a/acceptance/acceptance_test.go b/acceptance_test.go
similarity index 94%
rename from acceptance/acceptance_test.go
rename to acceptance_test.go
index 3b3b72f..3f1a7ea 100644
--- a/acceptance/acceptance_test.go
+++ b/acceptance_test.go
@@ -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
 
diff --git a/apk/apk_test.go b/apk/apk_test.go
index 0742e78..0f12b5a 100644
--- a/apk/apk_test.go
+++ b/apk/apk_test.go
@@ -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)
diff --git a/deb/deb_test.go b/deb/deb_test.go
index 5050c71..7ce7575 100644
--- a/deb/deb_test.go
+++ b/deb/deb_test.go
@@ -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)
diff --git a/files/files_test.go b/files/files_test.go
new file mode 100644
index 0000000..652e402
--- /dev/null
+++ b/files/files_test.go
@@ -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")
+}
diff --git a/internal/files/files_test.go b/internal/files/files_test.go
deleted file mode 100644
index 9911f64..0000000
--- a/internal/files/files_test.go
+++ /dev/null
@@ -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)
-}
diff --git a/nfpm_test.go b/nfpm_test.go
index 404178a..7447dd0 100644
--- a/nfpm_test.go
+++ b/nfpm_test.go
@@ -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
 }
diff --git a/rpm/rpm_test.go b/rpm/rpm_test.go
index 66084bc..a3ddfd2 100644
--- a/rpm/rpm_test.go
+++ b/rpm/rpm_test.go
@@ -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 {
diff --git a/acceptance/testdata/.gitignore b/testdata/acceptance/.gitignore
similarity index 100%
rename from acceptance/testdata/.gitignore
rename to testdata/acceptance/.gitignore
diff --git a/acceptance/testdata/apk.386.complex.dockerfile b/testdata/acceptance/apk.386.complex.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.386.complex.dockerfile
rename to testdata/acceptance/apk.386.complex.dockerfile
diff --git a/acceptance/testdata/apk.386.dockerfile b/testdata/acceptance/apk.386.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.386.dockerfile
rename to testdata/acceptance/apk.386.dockerfile
diff --git a/acceptance/testdata/apk.arm64.dockerfile b/testdata/acceptance/apk.arm64.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.arm64.dockerfile
rename to testdata/acceptance/apk.arm64.dockerfile
diff --git a/acceptance/testdata/apk.changelog.dockerfile b/testdata/acceptance/apk.changelog.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.changelog.dockerfile
rename to testdata/acceptance/apk.changelog.dockerfile
diff --git a/acceptance/testdata/apk.complex.dockerfile b/testdata/acceptance/apk.complex.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.complex.dockerfile
rename to testdata/acceptance/apk.complex.dockerfile
diff --git a/acceptance/testdata/apk.dockerfile b/testdata/acceptance/apk.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.dockerfile
rename to testdata/acceptance/apk.dockerfile
diff --git a/acceptance/testdata/apk.env-var-version.dockerfile b/testdata/acceptance/apk.env-var-version.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.env-var-version.dockerfile
rename to testdata/acceptance/apk.env-var-version.dockerfile
diff --git a/acceptance/testdata/apk.meta.dockerfile b/testdata/acceptance/apk.meta.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.meta.dockerfile
rename to testdata/acceptance/apk.meta.dockerfile
diff --git a/acceptance/testdata/apk.min.dockerfile b/testdata/acceptance/apk.min.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.min.dockerfile
rename to testdata/acceptance/apk.min.dockerfile
diff --git a/acceptance/testdata/apk.overrides.dockerfile b/testdata/acceptance/apk.overrides.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.overrides.dockerfile
rename to testdata/acceptance/apk.overrides.dockerfile
diff --git a/acceptance/testdata/apk.ppc64le.dockerfile b/testdata/acceptance/apk.ppc64le.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.ppc64le.dockerfile
rename to testdata/acceptance/apk.ppc64le.dockerfile
diff --git a/acceptance/testdata/apk.signed.dockerfile b/testdata/acceptance/apk.signed.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.signed.dockerfile
rename to testdata/acceptance/apk.signed.dockerfile
diff --git a/acceptance/testdata/apk.symlink.dockerfile b/testdata/acceptance/apk.symlink.dockerfile
similarity index 100%
rename from acceptance/testdata/apk.symlink.dockerfile
rename to testdata/acceptance/apk.symlink.dockerfile
diff --git a/acceptance/testdata/breaks.yaml b/testdata/acceptance/breaks.yaml
similarity index 100%
rename from acceptance/testdata/breaks.yaml
rename to testdata/acceptance/breaks.yaml
diff --git a/testdata/acceptance/complex.386.yaml b/testdata/acceptance/complex.386.yaml
new file mode 100644
index 0000000..d8946b4
--- /dev/null
+++ b/testdata/acceptance/complex.386.yaml
@@ -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
+
diff --git a/testdata/acceptance/complex.yaml b/testdata/acceptance/complex.yaml
new file mode 100644
index 0000000..6aed570
--- /dev/null
+++ b/testdata/acceptance/complex.yaml
@@ -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
diff --git a/acceptance/testdata/config-noreplace-old.yaml b/testdata/acceptance/config-noreplace-old.yaml
similarity index 65%
rename from acceptance/testdata/config-noreplace-old.yaml
rename to testdata/acceptance/config-noreplace-old.yaml
index bdf6851..6773597 100644
--- a/acceptance/testdata/config-noreplace-old.yaml
+++ b/testdata/acceptance/config-noreplace-old.yaml
@@ -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"
diff --git a/acceptance/testdata/config-noreplace.yaml b/testdata/acceptance/config-noreplace.yaml
similarity index 64%
rename from acceptance/testdata/config-noreplace.yaml
rename to testdata/acceptance/config-noreplace.yaml
index f45114d..ae5fe93 100644
--- a/acceptance/testdata/config-noreplace.yaml
+++ b/testdata/acceptance/config-noreplace.yaml
@@ -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"
diff --git a/acceptance/testdata/deb.386.complex.dockerfile b/testdata/acceptance/deb.386.complex.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.386.complex.dockerfile
rename to testdata/acceptance/deb.386.complex.dockerfile
diff --git a/acceptance/testdata/deb.386.dockerfile b/testdata/acceptance/deb.386.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.386.dockerfile
rename to testdata/acceptance/deb.386.dockerfile
diff --git a/acceptance/testdata/deb.arm64.dockerfile b/testdata/acceptance/deb.arm64.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.arm64.dockerfile
rename to testdata/acceptance/deb.arm64.dockerfile
diff --git a/acceptance/testdata/deb.breaks.dockerfile b/testdata/acceptance/deb.breaks.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.breaks.dockerfile
rename to testdata/acceptance/deb.breaks.dockerfile
diff --git a/acceptance/testdata/deb.changelog.dockerfile b/testdata/acceptance/deb.changelog.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.changelog.dockerfile
rename to testdata/acceptance/deb.changelog.dockerfile
diff --git a/acceptance/testdata/deb.complex.dockerfile b/testdata/acceptance/deb.complex.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.complex.dockerfile
rename to testdata/acceptance/deb.complex.dockerfile
diff --git a/acceptance/testdata/deb.dockerfile b/testdata/acceptance/deb.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.dockerfile
rename to testdata/acceptance/deb.dockerfile
diff --git a/acceptance/testdata/deb.env-var-version.dockerfile b/testdata/acceptance/deb.env-var-version.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.env-var-version.dockerfile
rename to testdata/acceptance/deb.env-var-version.dockerfile
diff --git a/acceptance/testdata/deb.meta.dockerfile b/testdata/acceptance/deb.meta.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.meta.dockerfile
rename to testdata/acceptance/deb.meta.dockerfile
diff --git a/acceptance/testdata/deb.min.dockerfile b/testdata/acceptance/deb.min.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.min.dockerfile
rename to testdata/acceptance/deb.min.dockerfile
diff --git a/acceptance/testdata/deb.overrides.dockerfile b/testdata/acceptance/deb.overrides.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.overrides.dockerfile
rename to testdata/acceptance/deb.overrides.dockerfile
diff --git a/acceptance/testdata/deb.ppc64le.dockerfile b/testdata/acceptance/deb.ppc64le.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.ppc64le.dockerfile
rename to testdata/acceptance/deb.ppc64le.dockerfile
diff --git a/acceptance/testdata/deb.signed.dockerfile b/testdata/acceptance/deb.signed.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.signed.dockerfile
rename to testdata/acceptance/deb.signed.dockerfile
diff --git a/acceptance/testdata/deb.symlink.dockerfile b/testdata/acceptance/deb.symlink.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.symlink.dockerfile
rename to testdata/acceptance/deb.symlink.dockerfile
diff --git a/acceptance/testdata/deb.triggers.dockerfile b/testdata/acceptance/deb.triggers.dockerfile
similarity index 100%
rename from acceptance/testdata/deb.triggers.dockerfile
rename to testdata/acceptance/deb.triggers.dockerfile
diff --git a/acceptance/testdata/dummy.deb b/testdata/acceptance/dummy.deb
similarity index 100%
rename from acceptance/testdata/dummy.deb
rename to testdata/acceptance/dummy.deb
diff --git a/testdata/acceptance/env-var-version.yaml b/testdata/acceptance/env-var-version.yaml
new file mode 100644
index 0000000..73b4ba7
--- /dev/null
+++ b/testdata/acceptance/env-var-version.yaml
@@ -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
diff --git a/acceptance/testdata/folder/file1 b/testdata/acceptance/folder/file1
similarity index 100%
rename from acceptance/testdata/folder/file1
rename to testdata/acceptance/folder/file1
diff --git a/acceptance/testdata/folder/file2 b/testdata/acceptance/folder/file2
similarity index 100%
rename from acceptance/testdata/folder/file2
rename to testdata/acceptance/folder/file2
diff --git a/acceptance/testdata/folder/folder2/file1 b/testdata/acceptance/folder/folder2/file1
similarity index 100%
rename from acceptance/testdata/folder/folder2/file1
rename to testdata/acceptance/folder/folder2/file1
diff --git a/acceptance/testdata/folder/folder2/file2 b/testdata/acceptance/folder/folder2/file2
similarity index 100%
rename from acceptance/testdata/folder/folder2/file2
rename to testdata/acceptance/folder/folder2/file2
diff --git a/acceptance/testdata/gzip.compression.yaml b/testdata/acceptance/gzip.compression.yaml
similarity index 64%
rename from acceptance/testdata/gzip.compression.yaml
rename to testdata/acceptance/gzip.compression.yaml
index cb0bdf9..a36dbb4 100644
--- a/acceptance/testdata/gzip.compression.yaml
+++ b/testdata/acceptance/gzip.compression.yaml
@@ -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"
\ No newline at end of file
diff --git a/acceptance/testdata/gzip.rpm.compression.dockerfile b/testdata/acceptance/gzip.rpm.compression.dockerfile
similarity index 100%
rename from acceptance/testdata/gzip.rpm.compression.dockerfile
rename to testdata/acceptance/gzip.rpm.compression.dockerfile
diff --git a/acceptance/testdata/keys/pubkey.asc b/testdata/acceptance/keys/pubkey.asc
similarity index 100%
rename from acceptance/testdata/keys/pubkey.asc
rename to testdata/acceptance/keys/pubkey.asc
diff --git a/acceptance/testdata/keys/pubkey.gpg b/testdata/acceptance/keys/pubkey.gpg
similarity index 100%
rename from acceptance/testdata/keys/pubkey.gpg
rename to testdata/acceptance/keys/pubkey.gpg
diff --git a/acceptance/testdata/keys/rsa_unprotected.pub b/testdata/acceptance/keys/rsa_unprotected.pub
similarity index 100%
rename from acceptance/testdata/keys/rsa_unprotected.pub
rename to testdata/acceptance/keys/rsa_unprotected.pub
diff --git a/acceptance/testdata/lzma.compression.yaml b/testdata/acceptance/lzma.compression.yaml
similarity index 64%
rename from acceptance/testdata/lzma.compression.yaml
rename to testdata/acceptance/lzma.compression.yaml
index 5b8de57..3883bec 100644
--- a/acceptance/testdata/lzma.compression.yaml
+++ b/testdata/acceptance/lzma.compression.yaml
@@ -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"
\ No newline at end of file
diff --git a/acceptance/testdata/lzma.rpm.compression.dockerfile b/testdata/acceptance/lzma.rpm.compression.dockerfile
similarity index 100%
rename from acceptance/testdata/lzma.rpm.compression.dockerfile
rename to testdata/acceptance/lzma.rpm.compression.dockerfile
diff --git a/acceptance/testdata/meta.yaml b/testdata/acceptance/meta.yaml
similarity index 100%
rename from acceptance/testdata/meta.yaml
rename to testdata/acceptance/meta.yaml
diff --git a/acceptance/testdata/min.yaml b/testdata/acceptance/min.yaml
similarity index 57%
rename from acceptance/testdata/min.yaml
rename to testdata/acceptance/min.yaml
index f4b9c64..eb5d377 100644
--- a/acceptance/testdata/min.yaml
+++ b/testdata/acceptance/min.yaml
@@ -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"
diff --git a/testdata/acceptance/overrides.yaml b/testdata/acceptance/overrides.yaml
new file mode 100644
index 0000000..3d7ab15
--- /dev/null
+++ b/testdata/acceptance/overrides.yaml
@@ -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
diff --git a/acceptance/testdata/release.rpm.dockerfile b/testdata/acceptance/release.rpm.dockerfile
similarity index 100%
rename from acceptance/testdata/release.rpm.dockerfile
rename to testdata/acceptance/release.rpm.dockerfile
diff --git a/acceptance/testdata/release.rpm.yaml b/testdata/acceptance/release.rpm.yaml
similarity index 70%
rename from acceptance/testdata/release.rpm.yaml
rename to testdata/acceptance/release.rpm.yaml
index 5ec5b26..cf0b6ac 100644
--- a/acceptance/testdata/release.rpm.yaml
+++ b/testdata/acceptance/release.rpm.yaml
@@ -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"
diff --git a/acceptance/testdata/rpm.386.complex.dockerfile b/testdata/acceptance/rpm.386.complex.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.386.complex.dockerfile
rename to testdata/acceptance/rpm.386.complex.dockerfile
diff --git a/acceptance/testdata/rpm.386.dockerfile b/testdata/acceptance/rpm.386.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.386.dockerfile
rename to testdata/acceptance/rpm.386.dockerfile
diff --git a/acceptance/testdata/rpm.arm64.dockerfile b/testdata/acceptance/rpm.arm64.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.arm64.dockerfile
rename to testdata/acceptance/rpm.arm64.dockerfile
diff --git a/acceptance/testdata/rpm.changelog.dockerfile b/testdata/acceptance/rpm.changelog.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.changelog.dockerfile
rename to testdata/acceptance/rpm.changelog.dockerfile
diff --git a/acceptance/testdata/rpm.complex.dockerfile b/testdata/acceptance/rpm.complex.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.complex.dockerfile
rename to testdata/acceptance/rpm.complex.dockerfile
diff --git a/acceptance/testdata/rpm.config-noreplace.dockerfile b/testdata/acceptance/rpm.config-noreplace.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.config-noreplace.dockerfile
rename to testdata/acceptance/rpm.config-noreplace.dockerfile
diff --git a/acceptance/testdata/rpm.dockerfile b/testdata/acceptance/rpm.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.dockerfile
rename to testdata/acceptance/rpm.dockerfile
diff --git a/acceptance/testdata/rpm.env-var-version.dockerfile b/testdata/acceptance/rpm.env-var-version.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.env-var-version.dockerfile
rename to testdata/acceptance/rpm.env-var-version.dockerfile
diff --git a/acceptance/testdata/rpm.meta.dockerfile b/testdata/acceptance/rpm.meta.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.meta.dockerfile
rename to testdata/acceptance/rpm.meta.dockerfile
diff --git a/acceptance/testdata/rpm.min.dockerfile b/testdata/acceptance/rpm.min.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.min.dockerfile
rename to testdata/acceptance/rpm.min.dockerfile
diff --git a/acceptance/testdata/rpm.overrides.dockerfile b/testdata/acceptance/rpm.overrides.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.overrides.dockerfile
rename to testdata/acceptance/rpm.overrides.dockerfile
diff --git a/acceptance/testdata/rpm.ppc64le.dockerfile b/testdata/acceptance/rpm.ppc64le.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.ppc64le.dockerfile
rename to testdata/acceptance/rpm.ppc64le.dockerfile
diff --git a/acceptance/testdata/rpm.signed.dockerfile b/testdata/acceptance/rpm.signed.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.signed.dockerfile
rename to testdata/acceptance/rpm.signed.dockerfile
diff --git a/acceptance/testdata/rpm.symlink.dockerfile b/testdata/acceptance/rpm.symlink.dockerfile
similarity index 100%
rename from acceptance/testdata/rpm.symlink.dockerfile
rename to testdata/acceptance/rpm.symlink.dockerfile
diff --git a/acceptance/testdata/rules.deb.dockerfile b/testdata/acceptance/rules.deb.dockerfile
similarity index 100%
rename from acceptance/testdata/rules.deb.dockerfile
rename to testdata/acceptance/rules.deb.dockerfile
diff --git a/acceptance/testdata/rules.deb.yaml b/testdata/acceptance/rules.deb.yaml
similarity index 61%
rename from acceptance/testdata/rules.deb.yaml
rename to testdata/acceptance/rules.deb.yaml
index 67e2c4c..d49da0f 100644
--- a/acceptance/testdata/rules.deb.yaml
+++ b/testdata/acceptance/rules.deb.yaml
@@ -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
diff --git a/acceptance/testdata/scripts/postinstall.sh b/testdata/acceptance/scripts/postinstall.sh
old mode 100755
new mode 100644
similarity index 100%
rename from acceptance/testdata/scripts/postinstall.sh
rename to testdata/acceptance/scripts/postinstall.sh
diff --git a/acceptance/testdata/scripts/postinstall_trigger.sh b/testdata/acceptance/scripts/postinstall_trigger.sh
similarity index 100%
rename from acceptance/testdata/scripts/postinstall_trigger.sh
rename to testdata/acceptance/scripts/postinstall_trigger.sh
diff --git a/acceptance/testdata/scripts/postremove.sh b/testdata/acceptance/scripts/postremove.sh
old mode 100755
new mode 100644
similarity index 100%
rename from acceptance/testdata/scripts/postremove.sh
rename to testdata/acceptance/scripts/postremove.sh
diff --git a/acceptance/testdata/scripts/preinstall.sh b/testdata/acceptance/scripts/preinstall.sh
old mode 100755
new mode 100644
similarity index 100%
rename from acceptance/testdata/scripts/preinstall.sh
rename to testdata/acceptance/scripts/preinstall.sh
diff --git a/acceptance/testdata/scripts/preremove.sh b/testdata/acceptance/scripts/preremove.sh
old mode 100755
new mode 100644
similarity index 100%
rename from acceptance/testdata/scripts/preremove.sh
rename to testdata/acceptance/scripts/preremove.sh
diff --git a/acceptance/testdata/scripts/rules.sh b/testdata/acceptance/scripts/rules.sh
old mode 100755
new mode 100644
similarity index 100%
rename from acceptance/testdata/scripts/rules.sh
rename to testdata/acceptance/scripts/rules.sh
diff --git a/acceptance/testdata/signed.yaml b/testdata/acceptance/signed.yaml
similarity index 52%
rename from acceptance/testdata/signed.yaml
rename to testdata/acceptance/signed.yaml
index d47a8c7..c77ee46 100644
--- a/acceptance/testdata/signed.yaml
+++ b/testdata/acceptance/signed.yaml
@@ -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
diff --git a/acceptance/testdata/simple.386.yaml b/testdata/acceptance/simple.386.yaml
similarity index 68%
rename from acceptance/testdata/simple.386.yaml
rename to testdata/acceptance/simple.386.yaml
index 2cf5d62..7a52923 100644
--- a/acceptance/testdata/simple.386.yaml
+++ b/testdata/acceptance/simple.386.yaml
@@ -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"
diff --git a/acceptance/testdata/simple.arm64.yaml b/testdata/acceptance/simple.arm64.yaml
similarity index 68%
rename from acceptance/testdata/simple.arm64.yaml
rename to testdata/acceptance/simple.arm64.yaml
index 0ad2bec..af7baae 100644
--- a/acceptance/testdata/simple.arm64.yaml
+++ b/testdata/acceptance/simple.arm64.yaml
@@ -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"
diff --git a/acceptance/testdata/simple.ppc64le.yaml b/testdata/acceptance/simple.ppc64le.yaml
similarity index 69%
rename from acceptance/testdata/simple.ppc64le.yaml
rename to testdata/acceptance/simple.ppc64le.yaml
index 9f57432..90b89f2 100644
--- a/acceptance/testdata/simple.ppc64le.yaml
+++ b/testdata/acceptance/simple.ppc64le.yaml
@@ -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"
diff --git a/acceptance/testdata/simple.yaml b/testdata/acceptance/simple.yaml
similarity index 70%
rename from acceptance/testdata/simple.yaml
rename to testdata/acceptance/simple.yaml
index f7e95db..96e1cf9 100644
--- a/acceptance/testdata/simple.yaml
+++ b/testdata/acceptance/simple.yaml
@@ -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"
diff --git a/acceptance/testdata/symlink.yaml b/testdata/acceptance/symlink.yaml
similarity index 79%
rename from acceptance/testdata/symlink.yaml
rename to testdata/acceptance/symlink.yaml
index b4c8e1b..4fb64d7 100644
--- a/acceptance/testdata/symlink.yaml
+++ b/testdata/acceptance/symlink.yaml
@@ -7,6 +7,6 @@ vendor: "foobar"
 homepage: "https://foobar.org"
 license: "MIT"
 files:
-  ../testdata/whatever.conf: "/etc/foo/whatever.conf"
+  ./testdata/whatever.conf: "/etc/foo/whatever.conf"
 symlinks:
   /path/to/symlink: "/etc/foo/whatever.conf"
diff --git a/acceptance/testdata/triggers.yaml b/testdata/acceptance/triggers.yaml
similarity index 63%
rename from acceptance/testdata/triggers.yaml
rename to testdata/acceptance/triggers.yaml
index 99a6c59..614f7d5 100644
--- a/acceptance/testdata/triggers.yaml
+++ b/testdata/acceptance/triggers.yaml
@@ -6,9 +6,9 @@ maintainer: "Foo Bar"
 vendor: "foobar"
 license: "MIT"
 files:
-  ../testdata/fake: "/usr/local/bin/fake"
+  ./testdata/fake: "/usr/local/bin/fake"
 scripts:
-  postinstall: ./testdata/scripts/postinstall_trigger.sh
+  postinstall: ./testdata/acceptance/scripts/postinstall_trigger.sh
 deb:
   triggers:
     interest:
diff --git a/acceptance/testdata/withchangelog.yaml b/testdata/acceptance/withchangelog.yaml
similarity index 62%
rename from acceptance/testdata/withchangelog.yaml
rename to testdata/acceptance/withchangelog.yaml
index 7d70f9b..6eb3683 100644
--- a/acceptance/testdata/withchangelog.yaml
+++ b/testdata/acceptance/withchangelog.yaml
@@ -1,7 +1,7 @@
 name: "foo"
 arch: "amd64"
 platform: "linux"
-changelog: "../testdata/changelog.yaml"
+changelog: "./testdata/changelog.yaml"
 version: "v1.2.3"
 release: "simple"
 maintainer: "Foo Bar"
@@ -12,6 +12,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"
diff --git a/acceptance/testdata/xz.compression.yaml b/testdata/acceptance/xz.compression.yaml
similarity index 65%
rename from acceptance/testdata/xz.compression.yaml
rename to testdata/acceptance/xz.compression.yaml
index f064775..db61cd6 100644
--- a/acceptance/testdata/xz.compression.yaml
+++ b/testdata/acceptance/xz.compression.yaml
@@ -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: "xz"
\ No newline at end of file
diff --git a/acceptance/testdata/xz.rpm.compression.dockerfile b/testdata/acceptance/xz.rpm.compression.dockerfile
similarity index 100%
rename from acceptance/testdata/xz.rpm.compression.dockerfile
rename to testdata/acceptance/xz.rpm.compression.dockerfile
diff --git a/testdata/contents.yaml b/testdata/contents.yaml
new file mode 100644
index 0000000..057ff91
--- /dev/null
+++ b/testdata/contents.yaml
@@ -0,0 +1,27 @@
+name: "contents foo"
+arch: "amd64"
+version: "v1.2.3"
+contents:
+  - src: "./testdata/*.conf"
+    dst: "/etc/foo/"
+    type: "config|noreplace"
+    file_info:
+      mode: 0644
+  - src: "./testdata/whatever2.conf"
+    dst: "/etc/rpm/whatever.conf"
+    type: "config|noreplace"
+    file_info:
+      mode: 0644
+      packager: "rpm"
+  - src: "./testdata/whatever2.conf"
+    dst: "/etc/apk/whatever.conf"
+    type: "config|noreplace"
+    file_info:
+      mode: 0644
+      packager: "apk"
+  - src: "./testdata/whatever2.conf"
+    dst: "/etc/deb/whatever.conf"
+    type: "config|noreplace"
+    file_info:
+      mode: 0644
+      packager: "deb"
\ No newline at end of file
diff --git a/testdata/contents_glob.yaml b/testdata/contents_glob.yaml
new file mode 100644
index 0000000..5a70872
--- /dev/null
+++ b/testdata/contents_glob.yaml
@@ -0,0 +1,10 @@
+# Configuration file used to unit test enhanced file map
+name: "foo"
+arch: "amd64"
+version: "v1.2.3"
+contents:
+  # These two should be a single glob `./testdata/globtest/**/*.txt`
+  - src: "./testdata/globtest/**/*.txt"
+    dst: "/etc/foo/"
+  - src: "./testdata/globtest/*.txt"
+    dst: "/etc/foo/"
\ No newline at end of file