Repository: goreleaser/nfpm
Pull request #218 (2020-08-24 18:59:45)

Fix: Fix deb file path prefixes and md5sums file
This pull request is based on #217 and ensures that files inside `.tar.gz` files of `deb` packages adhere to the same path prefix convention as regular upstream Debian packages. I created this PR to fix the broken acceptance test in #217 which was caused by the fact that `dpkg` in Ubuntu Docker images is configured to ignore changelog files but ONLY if we adhere to the path prefix conventions that are introduced in the PR.

During debugging I also found a nasty bug when generating the `md5sums` file if a changelog is added that results in a broken `md5sums` file. I included the fix and corresponding test in this PR because the changes were all lumped together after I was done debugging. Sorry for the mess.

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.


codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/nfpm/pull/218?src=pr&el=h1) Report
> Merging [#218](https://codecov.io/gh/goreleaser/nfpm/pull/218?src=pr&el=desc) into [master](https://codecov.io/gh/goreleaser/nfpm/commit/0c25629af794c020d89a9b3ea1e558d4fcfb9722?el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `56.66%`.

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

```diff
@@            Coverage Diff             @@
##           master     #218      +/-   ##
==========================================
- Coverage   69.00%   68.99%   -0.02%     
==========================================
  Files           7        7              
  Lines         926      932       +6     
==========================================
+ Hits          639      643       +4     
- Misses        178      179       +1     
- Partials      109      110       +1     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/nfpm/pull/218?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [deb/deb.go](https://codecov.io/gh/goreleaser/nfpm/pull/218/diff?src=pr&el=tree#diff-ZGViL2RlYi5nbw==) | `71.04% <56.66%> (-0.10%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/nfpm/pull/218?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/218?src=pr&el=footer). Last update [0c25629...8e460e4](https://codecov.io/gh/goreleaser/nfpm/pull/218?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/deb/deb.go b/deb/deb.go
index b77bd3f..fc1b07e 100644
--- a/deb/deb.go
+++ b/deb/deb.go
@@ -159,7 +159,7 @@ func createSymlinksInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[str
 		}
 
 		err := newItemInsideTarGz(out, []byte{}, &tar.Header{
-			Name:     strings.TrimLeft(src, "/"),
+			Name:     normalizePath(src),
 			Linkname: dst,
 			Typeflag: tar.TypeSymlink,
 			ModTime:  time.Now(),
@@ -203,27 +203,12 @@ func createFilesInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[string
 	}
 
 	if info.Changelog != "" {
-		changelog, err := createChangelog(info)
+		size, err := createChangelogInsideTarGz(out, &md5buf, created, info)
 		if err != nil {
 			return md5buf, 0, err
 		}
 
-		// https://www.debian.org/doc/manuals/developers-reference/pkgs.de.html#recording-changes-in-the-package
-		changelogName := fmt.Sprintf("/usr/share/doc/%s/changelog.gz", info.Name)
-		if err = createTree(out, changelogName, created); err != nil {
-			return md5buf, 0, err
-		}
-
-		var digest = md5.New() // nolint:gas
-		if _, err = fmt.Fprintf(&md5buf, "%x  %s\n", digest.Sum(nil), changelog); err != nil {
-			return md5buf, instSize, err
-		}
-
-		if err = newFileInsideTarGz(out, changelogName, changelog); err != nil {
-			return md5buf, instSize, err
-		}
-
-		instSize += int64(len(changelog))
+		instSize += size
 	}
 
 	return md5buf, instSize, nil
@@ -257,7 +242,7 @@ func copyToTarAndDigest(tarw *tar.Writer, md5w io.Writer, src, dst string) (int6
 		return 0, nil
 	}
 	var header = tar.Header{
-		Name:    filepath.ToSlash(dst[1:]),
+		Name:    normalizePath(dst),
 		Size:    info.Size(),
 		Mode:    int64(info.Mode()),
 		ModTime: time.Now(),
@@ -276,27 +261,49 @@ func copyToTarAndDigest(tarw *tar.Writer, md5w io.Writer, src, dst string) (int6
 	return info.Size(), nil
 }
 
-func createChangelog(info *nfpm.Info) (chglogTarGz []byte, err 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
 	// an error in another part of the code.
 	defer out.Close() // nolint: errcheck
 
-	chglogData, err := formatChangelog(info)
+	changelogContent, err := formatChangelog(info)
 	if err != nil {
-		return nil, err
+		return 0, err
 	}
 
-	if _, err = out.Write([]byte(chglogData)); err != nil {
-		return nil, err
+	if _, err = out.Write([]byte(changelogContent)); err != nil {
+		return 0, err
 	}
 
-	if err := out.Close(); err != nil {
-		return nil, errors.Wrap(err, "closing changelog.gz")
+	if err = out.Close(); err != nil {
+		return 0, errors.Wrap(err, "closing changelog.gz")
 	}
 
-	return buf.Bytes(), nil
+	changelogData := buf.Bytes()
+
+	// https://www.debian.org/doc/manuals/developers-reference/pkgs.de.html#recording-changes-in-the-package
+	changelogName := normalizePath(fmt.Sprintf("/usr/share/doc/%s/changelog.gz", info.Name))
+	if err = createTree(tarw, changelogName, created); err != nil {
+		return 0, err
+	}
+
+	var digest = md5.New() // nolint:gas
+	if _, err = digest.Write(changelogData); err != nil {
+		return 0, err
+	}
+
+	if _, err = fmt.Fprintf(md5w, "%x  %s\n", digest.Sum(nil), changelogName); err != nil {
+		return 0, err
+	}
+
+	if err = newFileInsideTarGz(tarw, changelogName, changelogData); err != nil {
+		return 0, err
+	}
+
+	return int64(len(changelogData)), nil
 }
 
 func formatChangelog(info *nfpm.Info) (string, error) {
@@ -397,7 +404,7 @@ func newItemInsideTarGz(out *tar.Writer, content []byte, header *tar.Header) err
 
 func newFileInsideTarGz(out *tar.Writer, name string, content []byte) error {
 	return newItemInsideTarGz(out, content, &tar.Header{
-		Name:     strings.TrimLeft(filepath.ToSlash(name), "/"),
+		Name:     normalizePath(name),
 		Size:     int64(len(content)),
 		Mode:     0644,
 		ModTime:  time.Now(),
@@ -406,6 +413,12 @@ func newFileInsideTarGz(out *tar.Writer, name string, content []byte) error {
 	})
 }
 
+// normalizePath returns a path separated by slashes, all relative path items
+// resolved and relative to the current directory (so it starts with "./").
+func normalizePath(src string) string {
+	return "." + filepath.ToSlash(filepath.Clean(filepath.Join("/", src)))
+}
+
 func newScriptInsideTarGz(out *tar.Writer, path, dest string) error {
 	file, err := os.Open(path) //nolint:gosec
 	if err != nil {
@@ -416,7 +429,7 @@ func newScriptInsideTarGz(out *tar.Writer, path, dest string) error {
 		return err
 	}
 	return newItemInsideTarGz(out, content, &tar.Header{
-		Name:     filepath.ToSlash(dest),
+		Name:     normalizePath(dest),
 		Size:     int64(len(content)),
 		Mode:     0755,
 		ModTime:  time.Now(),
@@ -429,13 +442,15 @@ func newScriptInsideTarGz(out *tar.Writer, path, dest string) error {
 // as well, so we walk through the dst and create all subfolders.
 func createTree(tarw *tar.Writer, dst string, created map[string]bool) error {
 	for _, path := range pathsToCreate(dst) {
+		path = normalizePath(path) + "/"
+
 		if created[path] {
 			// skipping dir that was previously created inside the archive
 			// (eg: usr/)
 			continue
 		}
 		if err := tarw.WriteHeader(&tar.Header{
-			Name:     filepath.ToSlash(path + "/"),
+			Name:     path,
 			Mode:     0755,
 			Typeflag: tar.TypeDir,
 			Format:   tar.FormatGNU,
@@ -450,7 +465,7 @@ func createTree(tarw *tar.Writer, dst string, created map[string]bool) error {
 
 func pathsToCreate(dst string) []string {
 	var paths = []string{}
-	var base = dst[1:]
+	var base = strings.TrimPrefix(dst, "/")
 	for {
 		base = filepath.Dir(base)
 		if base == "." {


Test patch:
diff --git a/acceptance/testdata/deb.changelog.dockerfile b/acceptance/testdata/deb.changelog.dockerfile
index cb4ca62..86c77bb 100644
--- a/acceptance/testdata/deb.changelog.dockerfile
+++ b/acceptance/testdata/deb.changelog.dockerfile
@@ -1,5 +1,9 @@
 FROM ubuntu
 ARG package
+# the dpkg configuration of the docker
+# image filters out changelogs by default
+# so we have to remove that rule
+RUN rm /etc/dpkg/dpkg.cfg.d/excludes
 COPY ${package} /tmp/foo.deb
 RUN apt upgrade -y
 RUN apt install -y gzip
diff --git a/deb/deb_test.go b/deb/deb_test.go
index e791ba1..8d5636b 100644
--- a/deb/deb_test.go
+++ b/deb/deb_test.go
@@ -4,6 +4,8 @@ import (
 	"archive/tar"
 	"bytes"
 	"compress/gzip"
+	"crypto/md5" // nolint: gosec
+	"encoding/hex"
 	"flag"
 	"fmt"
 	"io"
@@ -661,7 +663,7 @@ func TestSymlink(t *testing.T) {
 	assert.Equal(t, symlinkTarget, packagedSymlinkHeader.Linkname)
 }
 
-func TestNoLeadingSlashInTarGzFiles(t *testing.T) {
+func TestEnsureRelativePrefixInTarGzFiles(t *testing.T) {
 	info := exampleInfo()
 	info.Symlinks = map[string]string{
 		"/symlink/to/fake.txt": "/usr/share/doc/fake/fake.txt",
@@ -670,14 +672,48 @@ func TestNoLeadingSlashInTarGzFiles(t *testing.T) {
 
 	dataTarGz, md5sums, instSize, err := createDataTarGz(info)
 	require.NoError(t, err)
-	testNoLeadingSlashInTarGzFiles(t, dataTarGz)
+	testRelativePathPrefixInTarGzFiles(t, dataTarGz)
 
 	controlTarGz, err := createControl(instSize, md5sums, info)
 	require.NoError(t, err)
-	testNoLeadingSlashInTarGzFiles(t, controlTarGz)
+	testRelativePathPrefixInTarGzFiles(t, controlTarGz)
 }
 
-func testNoLeadingSlashInTarGzFiles(t *testing.T, tarGzFile []byte) {
+func TestMD5Sums(t *testing.T) {
+	info := exampleInfo()
+	info.Changelog = "../testdata/changelog.yaml"
+
+	nFiles := len(info.Files) + len(info.ConfigFiles) + 1 // +1 is the changelog
+
+	dataTarGz, md5sums, instSize, err := createDataTarGz(info)
+	require.NoError(t, err)
+
+	controlTarGz, err := createControl(instSize, md5sums, info)
+	require.NoError(t, err)
+
+	md5sumsFile, err := extractFileFromTarGz(controlTarGz, "./md5sums")
+	require.NoError(t, err)
+
+	lines := strings.Split(strings.TrimRight(string(md5sumsFile), "\n"), "\n")
+	require.Equal(t, nFiles, len(lines))
+
+	for _, line := range lines {
+		parts := strings.Fields(line)
+		require.Equal(t, len(parts), 2)
+
+		md5sum, fileName := parts[0], parts[1]
+
+		fileContent, err := extractFileFromTarGz(dataTarGz, fileName)
+		require.NoError(t, err)
+
+		digest := md5.New() // nolint:gosec
+		_, err = digest.Write(fileContent)
+		require.NoError(t, err)
+		assert.Equal(t, md5sum, hex.EncodeToString(digest.Sum(nil)))
+	}
+}
+
+func testRelativePathPrefixInTarGzFiles(t *testing.T, tarGzFile []byte) {
 	tarFile, err := gzipInflate(tarGzFile)
 	require.NoError(t, err)
 
@@ -689,7 +725,7 @@ func testNoLeadingSlashInTarGzFiles(t *testing.T, tarGzFile []byte) {
 		}
 		require.NoError(t, err)
 
-		assert.False(t, strings.HasPrefix(hdr.Name, "/"), "%s starts with /", hdr.Name)
+		assert.True(t, strings.HasPrefix(hdr.Name, "./"), "%s does not start with './'", hdr.Name)
 	}
 }