Repository: goreleaser/nfpm
Pull request #207 (2020-08-16 15:38:14)

feat: apk support
closes #126

--- 

its simpler than the original PR (no signature support).

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/ld564o42b](https://vercel.com/goreleaser/nfpm/ld564o42b)  
✅ Preview: [https://nfpm-git-apkplay.goreleaser.vercel.app](https://nfpm-git-apkplay.goreleaser.vercel.app)


codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/nfpm/pull/207?src=pr&el=h1) Report
> Merging [#207](https://codecov.io/gh/goreleaser/nfpm/pull/207?src=pr&el=desc) into [master](https://codecov.io/gh/goreleaser/nfpm/commit/62c61a56bd08eade24369b4ae819f459a463fc7f&el=desc) will **increase** coverage by `0.48%`.
> The diff coverage is `71.74%`.

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

```diff
@@            Coverage Diff             @@
##           master     #207      +/-   ##
==========================================
+ Coverage   69.76%   70.25%   +0.48%     
==========================================
  Files           6        7       +1     
  Lines         688      911     +223     
==========================================
+ Hits          480      640     +160     
- Misses        131      164      +33     
- Partials       77      107      +30     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/nfpm/pull/207?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [cmd/nfpm/main.go](https://codecov.io/gh/goreleaser/nfpm/pull/207/diff?src=pr&el=tree#diff-Y21kL25mcG0vbWFpbi5nbw==) | `0.00% <ø> (ø)` | |
| [apk/apk.go](https://codecov.io/gh/goreleaser/nfpm/pull/207/diff?src=pr&el=tree#diff-YXBrL2Fway5nbw==) | `71.74% <71.74%> (ø)` | |

------

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


Patch:
diff --git a/Makefile b/Makefile
index 2ceda57..2deb2f3 100644
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,7 @@ test:
 .PHONY: test
 
 cover: test
-	go tool cover -html=coverage.out
+	go tool cover -html=coverage.txt
 .PHONY: cover
 
 fmt:
diff --git a/README.md b/README.md
index cacca15..ed8e438 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
   <p align="center">
     <a href="https://github.com/goreleaser/nfpm/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/goreleaser/nfpm.svg?style=for-the-badge"></a>
     <a href="/LICENSE.md"><img alt="Software License" src="https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge"></a>
-    <a href="https://github.com/goreleaser/goreleaser/actions?workflow=build"><img alt="GitHub Actions" src="https://img.shields.io/github/workflow/status/goreleaser/goreleaser/build?style=for-the-badge"></a>
+    <a href="https://github.com/goreleaser/nfpm/actions?workflow=build"><img alt="GitHub Actions" src="https://img.shields.io/github/workflow/status/goreleaser/nfpm/build?style=for-the-badge"></a>
     <a href="https://codecov.io/gh/goreleaser/nfpm"><img alt="Codecov branch" src="https://img.shields.io/codecov/c/github/goreleaser/nfpm/master.svg?style=for-the-badge"></a>
     <a href="https://goreportcard.com/report/github.com/goreleaser/nfpm"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/goreleaser/nfpm?style=for-the-badge"></a>
     <a href="http://godoc.org/github.com/goreleaser/nfpm"><img alt="Go Doc" src="https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge"></a>
diff --git a/apk/apk.go b/apk/apk.go
new file mode 100644
index 0000000..9f40df2
--- /dev/null
+++ b/apk/apk.go
@@ -0,0 +1,494 @@
+/*
+Copyright 2019 Torsten Curdt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+// Package apk (someday) implements nfpm.Packager providing .apk bindings.
+package apk
+
+// Initial implementation from https://gist.github.com/tcurdt/512beaac7e9c12dcf5b6b7603b09d0d8
+
+import (
+	"archive/tar"
+	"bufio"
+	"bytes"
+	"compress/gzip"
+	"crypto/sha256"
+	"fmt"
+	"hash"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync/atomic"
+	"text/template"
+	"time"
+
+	"github.com/goreleaser/nfpm/internal/files"
+
+	"github.com/pkg/errors"
+
+	"github.com/goreleaser/nfpm"
+)
+
+// nolint: gochecknoinits
+func init() {
+	nfpm.Register("apk", Default)
+}
+
+// nolint: gochecknoglobals
+var archToAlpine = map[string]string{
+	"386":   "x86",
+	"amd64": "x86_64",
+
+	"arm":   "armhf",
+	"arm6":  "armhf",
+	"arm7":  "armhf",
+	"arm64": "aarch64",
+
+	// "s390x":  "???",
+}
+
+// Default apk packager
+// nolint: gochecknoglobals
+var Default = &Apk{}
+
+// Apk is a apk packager implementation.
+type Apk struct{}
+
+func (a *Apk) ConventionalFileName(info *nfpm.Info) string {
+	// TODO: verify this
+	return fmt.Sprintf("%s_%s_%s.apk", info.Name, info.Version, info.Arch)
+}
+
+// Package writes a new apk package to the given writer using the given info.
+func (*Apk) Package(info *nfpm.Info, apk io.Writer) (err error) {
+	arch, ok := archToAlpine[info.Arch]
+	if ok {
+		info.Arch = arch
+	}
+
+	var bufData bytes.Buffer
+
+	size := int64(0)
+	// create the data tgz
+	_, err = createData(&bufData, info, &size)
+	if err != nil {
+		return err
+	}
+
+	// create the control tgz
+	var bufControl bytes.Buffer
+	if _, err = createControl(&bufControl, info, size); err != nil {
+		return err
+	}
+
+	// combine
+	return combineToApk(apk, &bufData, &bufControl)
+}
+
+type writerCounter struct {
+	io.Writer
+	count  uint64
+	writer io.Writer
+}
+
+func newWriterCounter(w io.Writer) *writerCounter {
+	return &writerCounter{
+		writer: w,
+	}
+}
+
+func (counter *writerCounter) Write(buf []byte) (int, error) {
+	n, err := counter.writer.Write(buf)
+	atomic.AddUint64(&counter.count, uint64(n))
+	return n, err
+}
+
+func (counter *writerCounter) Count() uint64 {
+	return atomic.LoadUint64(&counter.count)
+}
+
+func writeFile(tw *tar.Writer, header *tar.Header, file io.Reader) error {
+	header.Format = tar.FormatUSTAR
+	header.ChangeTime = time.Time{}
+	header.AccessTime = time.Time{}
+
+	err := tw.WriteHeader(header)
+	if err != nil {
+		return err
+	}
+
+	_, err = io.Copy(tw, file)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+type tarKind int
+
+const (
+	tarFull tarKind = iota
+	tarCut
+)
+
+func writeTgz(w io.Writer, kind tarKind, builder func(tw *tar.Writer) error, digest hash.Hash) ([]byte, error) {
+	mw := io.MultiWriter(digest, w)
+	gw := gzip.NewWriter(mw)
+	cw := newWriterCounter(gw)
+	bw := bufio.NewWriterSize(cw, 4096)
+	tw := tar.NewWriter(bw)
+
+	err := builder(tw)
+	if err != nil {
+		return nil, err
+	}
+
+	// handle the cut vs full tars
+	if err = bw.Flush(); err != nil {
+		return nil, err
+	}
+	if err = tw.Close(); err != nil {
+		return nil, err
+	}
+	if kind == tarFull {
+		if err = bw.Flush(); err != nil {
+			return nil, err
+		}
+	}
+
+	size := cw.Count()
+	alignedSize := (size + 511) & ^uint64(511)
+
+	increase := alignedSize - size
+	if increase > 0 {
+		b := make([]byte, increase)
+		_, err = cw.Write(b)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if err = gw.Close(); err != nil {
+		return nil, err
+	}
+
+	return digest.Sum(nil), nil
+}
+
+func createData(dataTgz io.Writer, info *nfpm.Info, sizep *int64) ([]byte, error) {
+	builderData := createBuilderData(info, sizep)
+	dataDigest, err := writeTgz(dataTgz, tarFull, builderData, sha256.New())
+	if err != nil {
+		return nil, err
+	}
+	return dataDigest, nil
+}
+
+func createControl(controlTgz io.Writer, info *nfpm.Info, size int64) ([]byte, error) {
+	builderControl := createBuilderControl(info, size)
+	controlDigest, err := writeTgz(controlTgz, tarCut, builderControl, sha256.New())
+	if err != nil {
+		return nil, err
+	}
+	return controlDigest, nil
+}
+
+func combineToApk(target io.Writer, dataTgz, controlTgz io.Reader) error {
+	for _, tgz := range []io.Reader{controlTgz, dataTgz} {
+		if _, err := io.Copy(target, tgz); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func createBuilderControl(info *nfpm.Info, size int64) func(tw *tar.Writer) error {
+	return func(tw *tar.Writer) error {
+		var infoBuf bytes.Buffer
+		if err := writeControl(&infoBuf, controlData{
+			Info:          info,
+			InstalledSize: size,
+		}); err != nil {
+			return err
+		}
+		infoContent := infoBuf.String()
+
+		infoHeader := &tar.Header{
+			Name: ".PKGINFO",
+			Mode: 0600,
+			Size: int64(len(infoContent)),
+		}
+
+		if err := writeFile(tw, infoHeader, strings.NewReader(infoContent)); err != nil {
+			return err
+		}
+
+		// NOTE: Apk scripts tend to follow the pattern:
+		// #!/bin/sh
+		//
+		// bin/echo 'running preinstall.sh' // do stuff here
+		//
+		// exit 0
+		for script, dest := range map[string]string{
+			info.Scripts.PreInstall:  ".pre-install",
+			info.Scripts.PostInstall: ".post-install",
+			info.Scripts.PreRemove:   ".pre-deinstall",
+			info.Scripts.PostRemove:  ".post-deinstall",
+		} {
+			if script != "" {
+				if err := newScriptInsideTarGz(tw, script, dest); err != nil {
+					return err
+				}
+			}
+		}
+
+		return nil
+	}
+}
+
+func newScriptInsideTarGz(out *tar.Writer, path, dest string) error {
+	file, err := os.Open(path) //nolint:gosec
+	if err != nil {
+		return err
+	}
+	content, err := ioutil.ReadAll(file)
+	if err != nil {
+		return err
+	}
+	return newItemInsideTarGz(out, content, &tar.Header{
+		Name:     filepath.ToSlash(dest),
+		Size:     int64(len(content)),
+		Mode:     0755,
+		ModTime:  time.Now(),
+		Typeflag: tar.TypeReg,
+		Format:   tar.FormatGNU,
+	})
+}
+
+func newItemInsideTarGz(out *tar.Writer, content []byte, header *tar.Header) error {
+	if err := out.WriteHeader(header); err != nil {
+		return errors.Wrapf(err, "cannot write header of %s file to control.tar.gz", header.Name)
+	}
+	if _, err := out.Write(content); err != nil {
+		return errors.Wrapf(err, "cannot write %s file to control.tar.gz", header.Name)
+	}
+	return nil
+}
+
+func createBuilderData(info *nfpm.Info, sizep *int64) func(tw *tar.Writer) error {
+	var created = map[string]bool{}
+
+	return func(tw *tar.Writer) error {
+		// handle empty folders
+		if err := createEmptyFoldersInsideTarGz(info, tw, created); err != nil {
+			return err
+		}
+
+		// handle Files and ConfigFiles
+		if err := createFilesInsideTarGz(info, tw, created, sizep); err != nil {
+			return err
+		}
+
+		return createSymlinksInsideTarGz(info, tw, created)
+	}
+}
+
+func createFilesInsideTarGz(info *nfpm.Info, tw *tar.Writer, created map[string]bool, sizep *int64) error {
+	filesToCopy, err := files.Expand(info.Files)
+	if err != nil {
+		return err
+	}
+
+	configFilesToCopy, err := files.Expand(info.ConfigFiles)
+	if err != nil {
+		return err
+	}
+
+	for _, file := range append(filesToCopy, configFilesToCopy...) {
+		if err = createTree(tw, file.Destination, created); err != nil {
+			return err
+		}
+		err := copyToTarAndDigest(file.Source, file.Destination, tw, sizep, created)
+		if err != nil {
+			return err
+		}
+	}
+
+	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 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
+	if err != nil {
+		return errors.Wrap(err, "could not add file to the archive")
+	}
+	// 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)
+	if err != nil {
+		log.Print(err)
+		return err
+	}
+
+	header.Name = filepath.ToSlash(dst[1:])
+	err = writeFile(tw, header, file)
+	if err != nil {
+		return err
+	}
+
+	*sizep += info.Size()
+	created[src] = true
+	return nil
+}
+
+func createEmptyFoldersInsideTarGz(info *nfpm.Info, out *tar.Writer, created map[string]bool) error {
+	for _, folder := range info.EmptyFolders {
+		// this .nope is actually not created, because createTree ignore the
+		// last part of the path, assuming it is a file.
+		// TODO: should probably refactor this
+		if err := createTree(out, filepath.Join(folder, ".nope"), created); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// this is needed because the data.tar.gz file should have the empty folders
+// 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) {
+		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 + "/"),
+			Mode:     0755,
+			Typeflag: tar.TypeDir,
+			Format:   tar.FormatGNU,
+			ModTime:  time.Now(),
+		}); err != nil {
+			return errors.Wrap(err, "failed to create folder")
+		}
+		created[path] = true
+	}
+	return nil
+}
+
+func pathsToCreate(dst string) []string {
+	var paths []string
+	var base = dst[1:]
+	for {
+		base = filepath.Dir(base)
+		if base == "." {
+			break
+		}
+		paths = append(paths, base)
+	}
+	// we don't really need to create those things in order apparently, but,
+	// it looks really weird if we don't.
+	var result []string
+	for i := len(paths) - 1; i >= 0; i-- {
+		result = append(result, paths[i])
+	}
+	return result
+}
+
+// reference: https://wiki.adelielinux.org/wiki/APK_internals#.PKGINFO
+const controlTemplate = `
+{{- /* Mandatory fields */ -}}
+pkgname = {{.Info.Name}}
+pkgver = {{ if .Info.Epoch}}{{ .Info.Epoch }}:{{ end }}{{.Info.Version}}
+         {{- if .Info.Release}}-{{ .Info.Release }}{{- end }}
+         {{- if .Info.Prerelease}}~{{ .Info.Prerelease }}{{- end }}
+         {{- if .Info.Deb.VersionMetadata}}+{{ .Info.Deb.VersionMetadata }}{{- end }}
+arch = {{.Info.Arch}}
+size = {{.InstalledSize}}
+pkgdesc = {{multiline .Info.Description}}
+{{- if .Info.Homepage}}
+url = {{.Info.Homepage}}
+{{- end }}
+{{- if .Info.Maintainer}}
+maintainer = {{.Info.Maintainer}}
+{{- end }}
+{{- range $repl := .Info.Replaces}}
+replaces = {{ $repl }}
+{{- end }}
+{{- range $prov := .Info.Provides}}
+provides = {{ $prov }}
+{{- end }}
+{{- range $dep := .Info.Depends}}
+depend = {{ $dep }}
+{{- end }}
+{{- if .Info.License}}
+license = {{.Info.License}}
+{{- end }}
+`
+
+type controlData struct {
+	Info          *nfpm.Info
+	InstalledSize int64
+}
+
+func writeControl(w io.Writer, data controlData) error {
+	var tmpl = template.New("control")
+	tmpl.Funcs(template.FuncMap{
+		"multiline": func(strs string) string {
+			ret := strings.ReplaceAll(strs, "\n", "\n  ")
+			return strings.Trim(ret, " \n")
+		},
+	})
+	return template.Must(tmpl.Parse(controlTemplate)).Execute(w, data)
+}
diff --git a/cmd/nfpm/main.go b/cmd/nfpm/main.go
index a6a3fca..fc2d4c8 100644
--- a/cmd/nfpm/main.go
+++ b/cmd/nfpm/main.go
@@ -12,6 +12,7 @@ import (
 	"github.com/alecthomas/kingpin"
 
 	"github.com/goreleaser/nfpm"
+	_ "github.com/goreleaser/nfpm/apk"
 	_ "github.com/goreleaser/nfpm/deb"
 	_ "github.com/goreleaser/nfpm/rpm"
 )


Test patch:
diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go
index edfb5fe..d49a7ef 100644
--- a/acceptance/acceptance_test.go
+++ b/acceptance/acceptance_test.go
@@ -13,12 +13,13 @@ import (
 
 	"github.com/goreleaser/nfpm"
 	// shut up
+	_ "github.com/goreleaser/nfpm/apk"
 	_ "github.com/goreleaser/nfpm/deb"
 	_ "github.com/goreleaser/nfpm/rpm"
 )
 
 // nolint: gochecknoglobals
-var formats = []string{"deb", "rpm"}
+var formats = []string{"deb", "rpm", "apk"}
 
 func TestSimple(t *testing.T) {
 	for _, format := range formats {
@@ -261,6 +262,7 @@ func accept(t *testing.T, params acceptParms) {
 	require.NoError(t, err)
 	var packageName = params.Name + "." + params.Format
 	var target = filepath.Join(tmp, packageName)
+	t.Log("package: " + target)
 
 	require.NoError(t, os.MkdirAll(tmp, 0700))
 
diff --git a/acceptance/testdata/apk.386.complex.dockerfile b/acceptance/testdata/apk.386.complex.dockerfile
new file mode 100644
index 0000000..d9dbf83
--- /dev/null
+++ b/acceptance/testdata/apk.386.complex.dockerfile
@@ -0,0 +1,27 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+RUN test -e /usr/local/bin/fake
+RUN test -f /etc/foo/whatever.conf
+RUN test -d /usr/share/whatever/
+RUN test -d /usr/share/whatever/folder
+RUN test -f /usr/share/whatever/folder/file1
+RUN test -f /usr/share/whatever/folder/file2
+RUN test -d /usr/share/whatever/folder/folder2
+RUN test -f /usr/share/whatever/folder/folder2/file1
+RUN test -f /usr/share/whatever/folder/folder2/file2
+RUN test -d /usr/share/foo
+RUN test -d /usr/share/whatever
+RUN test -f /tmp/preinstall-proof
+RUN test -f /tmp/postinstall-proof
+RUN test ! -f /tmp/preremove-proof
+RUN test ! -f /tmp/postremove-proof
+RUN echo wat >> /etc/foo/whatever.conf
+RUN apk del foo
+RUN test -f /etc/foo/whatever.conf
+RUN test ! -f /usr/local/bin/fake
+RUN test -f /tmp/preremove-proof
+RUN test -f /tmp/postremove-proof
+RUN test ! -d /usr/share/foo
+RUN test ! -d /usr/share/whatever
diff --git a/acceptance/testdata/apk.386.dockerfile b/acceptance/testdata/apk.386.dockerfile
new file mode 100644
index 0000000..7f1a9f1
--- /dev/null
+++ b/acceptance/testdata/apk.386.dockerfile
@@ -0,0 +1,10 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+RUN test -e /usr/local/bin/fake
+RUN test -f /etc/foo/whatever.conf
+RUN echo wat >> /etc/foo/whatever.conf
+RUN apk del foo
+RUN test -f /etc/foo/whatever.conf
+RUN test ! -f /usr/local/bin/fake
diff --git a/acceptance/testdata/apk.arm64.dockerfile b/acceptance/testdata/apk.arm64.dockerfile
new file mode 100644
index 0000000..7f1a9f1
--- /dev/null
+++ b/acceptance/testdata/apk.arm64.dockerfile
@@ -0,0 +1,10 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+RUN test -e /usr/local/bin/fake
+RUN test -f /etc/foo/whatever.conf
+RUN echo wat >> /etc/foo/whatever.conf
+RUN apk del foo
+RUN test -f /etc/foo/whatever.conf
+RUN test ! -f /usr/local/bin/fake
diff --git a/acceptance/testdata/apk.changelog.dockerfile b/acceptance/testdata/apk.changelog.dockerfile
new file mode 100644
index 0000000..e115a0a
--- /dev/null
+++ b/acceptance/testdata/apk.changelog.dockerfile
@@ -0,0 +1,5 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+# TODO: seems like there is no changelog support on apk
diff --git a/acceptance/testdata/apk.complex.dockerfile b/acceptance/testdata/apk.complex.dockerfile
new file mode 100644
index 0000000..95a1262
--- /dev/null
+++ b/acceptance/testdata/apk.complex.dockerfile
@@ -0,0 +1,27 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+RUN test -e /usr/local/bin/fake
+RUN test -f /etc/foo/whatever.conf
+RUN test -d /usr/share/whatever/
+RUN test -d /usr/share/whatever/folder
+RUN test -f /usr/share/whatever/folder/file1
+RUN test -f /usr/share/whatever/folder/file2
+RUN test -d /usr/share/whatever/folder/folder2
+RUN test -f /usr/share/whatever/folder/folder2/file1
+RUN test -f /usr/share/whatever/folder/folder2/file2
+RUN test -d /var/log/whatever
+RUN test -d /usr/share/foo
+RUN test -f /tmp/preinstall-proof
+RUN test -f /tmp/postinstall-proof
+RUN test ! -f /tmp/preremove-proof
+RUN test ! -f /tmp/postremove-proof
+RUN echo wat >> /etc/foo/whatever.conf
+RUN apk del foo
+RUN test -f /etc/foo/whatever.conf
+RUN test ! -f /usr/local/bin/fake
+RUN test -f /tmp/preremove-proof
+RUN test -f /tmp/postremove-proof
+RUN test ! -d /var/log/whatever
+RUN test ! -d /usr/share/foo
diff --git a/acceptance/testdata/apk.dockerfile b/acceptance/testdata/apk.dockerfile
new file mode 100644
index 0000000..7f1a9f1
--- /dev/null
+++ b/acceptance/testdata/apk.dockerfile
@@ -0,0 +1,10 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+RUN test -e /usr/local/bin/fake
+RUN test -f /etc/foo/whatever.conf
+RUN echo wat >> /etc/foo/whatever.conf
+RUN apk del foo
+RUN test -f /etc/foo/whatever.conf
+RUN test ! -f /usr/local/bin/fake
diff --git a/acceptance/testdata/apk.env-var-version.dockerfile b/acceptance/testdata/apk.env-var-version.dockerfile
new file mode 100644
index 0000000..6f05a9e
--- /dev/null
+++ b/acceptance/testdata/apk.env-var-version.dockerfile
@@ -0,0 +1,9 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+ENV EXPECTVER="foo-1.0.0~0.1.b1 description:"
+RUN apk info foo | grep "foo-" | grep " description:" > found
+RUN export FOUND_VER="$(cat found)" && \
+    echo "Expected: '${EXPECTVER}' :: Found: '${FOUND_VER}'" && \
+    test "${FOUND_VER}" = "${EXPECTVER}"
diff --git a/acceptance/testdata/apk.meta.dockerfile b/acceptance/testdata/apk.meta.dockerfile
new file mode 100644
index 0000000..5b2d96c
--- /dev/null
+++ b/acceptance/testdata/apk.meta.dockerfile
@@ -0,0 +1,6 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+RUN command -v zsh
+RUN command -v fish
diff --git a/acceptance/testdata/apk.min.dockerfile b/acceptance/testdata/apk.min.dockerfile
new file mode 100644
index 0000000..7b5b608
--- /dev/null
+++ b/acceptance/testdata/apk.min.dockerfile
@@ -0,0 +1,4 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
diff --git a/acceptance/testdata/apk.overrides.dockerfile b/acceptance/testdata/apk.overrides.dockerfile
new file mode 100644
index 0000000..2860cc6
--- /dev/null
+++ b/acceptance/testdata/apk.overrides.dockerfile
@@ -0,0 +1,16 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add -vvv --allow-untrusted /tmp/foo.apk
+RUN test -e /usr/local/bin/fake
+RUN test -f /etc/foo/whatever.conf
+RUN test ! -f /tmp/preinstall-proof
+RUN test -f /tmp/postinstall-proof
+RUN test ! -f /tmp/preremove-proof
+RUN test ! -f /tmp/postremove-proof
+RUN echo wat >> /etc/foo/whatever.conf
+RUN apk del foo
+RUN test -f /etc/foo/whatever.conf
+RUN test ! -f /usr/local/bin/fake
+RUN test -f /tmp/preremove-proof
+RUN test ! -f /tmp/postremove-proof
diff --git a/acceptance/testdata/apk.ppc64le.dockerfile b/acceptance/testdata/apk.ppc64le.dockerfile
new file mode 100644
index 0000000..7f1a9f1
--- /dev/null
+++ b/acceptance/testdata/apk.ppc64le.dockerfile
@@ -0,0 +1,10 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+RUN test -e /usr/local/bin/fake
+RUN test -f /etc/foo/whatever.conf
+RUN echo wat >> /etc/foo/whatever.conf
+RUN apk del foo
+RUN test -f /etc/foo/whatever.conf
+RUN test ! -f /usr/local/bin/fake
diff --git a/acceptance/testdata/apk.symlink.dockerfile b/acceptance/testdata/apk.symlink.dockerfile
new file mode 100644
index 0000000..345d01b
--- /dev/null
+++ b/acceptance/testdata/apk.symlink.dockerfile
@@ -0,0 +1,5 @@
+FROM alpine
+ARG package
+COPY ${package} /tmp/foo.apk
+RUN apk add --allow-untrusted /tmp/foo.apk
+RUN ls -l /path/to/symlink | grep "/path/to/symlink -> /etc/foo/whatever.conf"
diff --git a/acceptance/testdata/complex.386.yaml b/acceptance/testdata/complex.386.yaml
index 97d2633..50f8cfd 100644
--- a/acceptance/testdata/complex.386.yaml
+++ b/acceptance/testdata/complex.386.yaml
@@ -30,3 +30,4 @@ scripts:
   postinstall: ./testdata/scripts/postinstall.sh
   preremove: ./testdata/scripts/preremove.sh
   postremove: ./testdata/scripts/postremove.sh
+
diff --git a/acceptance/testdata/deb.changelog.dockerfile b/acceptance/testdata/deb.changelog.dockerfile
index 535d234..cb4ca62 100644
--- a/acceptance/testdata/deb.changelog.dockerfile
+++ b/acceptance/testdata/deb.changelog.dockerfile
@@ -8,4 +8,3 @@ RUN zcat "/usr/share/doc/foo/changelog.gz" | grep "Carlos A Becker <pkg@carlosbe
 RUN zcat "/usr/share/doc/foo/changelog.gz" | grep "note 1"
 RUN zcat "/usr/share/doc/foo/changelog.gz" | grep "note 2"
 RUN zcat "/usr/share/doc/foo/changelog.gz" | grep "note 3"
-RUN dpkg -r foo
diff --git a/acceptance/testdata/deb.min.dockerfile b/acceptance/testdata/deb.min.dockerfile
index 8d8f89b..480b261 100644
--- a/acceptance/testdata/deb.min.dockerfile
+++ b/acceptance/testdata/deb.min.dockerfile
@@ -2,4 +2,3 @@ FROM ubuntu
 ARG package
 COPY ${package} /tmp/foo.deb
 RUN dpkg -i /tmp/foo.deb
-RUN dpkg -r foo
diff --git a/acceptance/testdata/deb.symlink.dockerfile b/acceptance/testdata/deb.symlink.dockerfile
index d5870e1..3dbfcbf 100644
--- a/acceptance/testdata/deb.symlink.dockerfile
+++ b/acceptance/testdata/deb.symlink.dockerfile
@@ -3,4 +3,3 @@ ARG package
 COPY ${package} /tmp/foo.deb
 RUN dpkg -i /tmp/foo.deb
 RUN ls -l /path/to/symlink | grep "/path/to/symlink -> /etc/foo/whatever.conf"
-RUN dpkg -r foo
diff --git a/acceptance/testdata/deb.triggers.dockerfile b/acceptance/testdata/deb.triggers.dockerfile
index 4b0c8d5..6e37e77 100644
--- a/acceptance/testdata/deb.triggers.dockerfile
+++ b/acceptance/testdata/deb.triggers.dockerfile
@@ -6,7 +6,4 @@ RUN dpkg -i /tmp/foo.deb
 # simulate another package that activates the trigger
 RUN dpkg-trigger --by-package foo manual-trigger
 RUN dpkg --triggers-only foo
-
 RUN test -f /tmp/trigger-proof
-
-RUN dpkg -r foo
diff --git a/acceptance/testdata/overrides.yaml b/acceptance/testdata/overrides.yaml
index 8610ea5..a0f1f3c 100644
--- a/acceptance/testdata/overrides.yaml
+++ b/acceptance/testdata/overrides.yaml
@@ -31,3 +31,7 @@ overrides:
     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/testdata/rpm.changelog.dockerfile b/acceptance/testdata/rpm.changelog.dockerfile
index c622d79..216a660 100644
--- a/acceptance/testdata/rpm.changelog.dockerfile
+++ b/acceptance/testdata/rpm.changelog.dockerfile
@@ -10,4 +10,3 @@ RUN rpm -q foo --changelog | grep "Carlos A Becker <pkg@carlosbecker.com>"
 RUN rpm -q foo --changelog | grep -E "^- note 1$"
 RUN rpm -q foo --changelog | grep -E "^- note 2$"
 RUN rpm -q foo --changelog | grep -E "^- note 3$"
-RUN rpm -e foo
diff --git a/acceptance/testdata/rpm.config-noreplace.dockerfile b/acceptance/testdata/rpm.config-noreplace.dockerfile
index 6b9c39e..a48280a 100644
--- a/acceptance/testdata/rpm.config-noreplace.dockerfile
+++ b/acceptance/testdata/rpm.config-noreplace.dockerfile
@@ -17,5 +17,3 @@ RUN cat /etc/regular.conf.rpmsave | grep modified
 RUN cat /etc/noreplace.conf | grep modified
 RUN test -f /etc/noreplace.conf.rpmnew
 RUN cat /etc/noreplace.conf.rpmnew | grep foo=baz
-
-RUN rpm -e foo
\ No newline at end of file
diff --git a/acceptance/testdata/rpm.symlink.dockerfile b/acceptance/testdata/rpm.symlink.dockerfile
index 8cccade..fb21044 100644
--- a/acceptance/testdata/rpm.symlink.dockerfile
+++ b/acceptance/testdata/rpm.symlink.dockerfile
@@ -3,4 +3,3 @@ ARG package
 COPY ${package} /tmp/foo.rpm
 RUN rpm -ivh /tmp/foo.rpm
 RUN ls -l /path/to/symlink | grep "/path/to/symlink -> /etc/foo/whatever.conf"
-RUN rpm -e foo
diff --git a/apk/apk_test.go b/apk/apk_test.go
new file mode 100644
index 0000000..a77d006
--- /dev/null
+++ b/apk/apk_test.go
@@ -0,0 +1,281 @@
+package apk
+
+import (
+	"archive/tar"
+	"bytes"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/goreleaser/nfpm"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// nolint: gochecknoglobals
+var update = flag.Bool("update", false, "update apk .golden files")
+
+func exampleInfo() *nfpm.Info {
+	return nfpm.WithDefaults(&nfpm.Info{
+		Name:        "foo",
+		Arch:        "amd64",
+		Description: "Foo does things",
+		Priority:    "extra",
+		Maintainer:  "Carlos A Becker <pkg@carlosbecker.com>",
+		Version:     "v1.0.0",
+		Release:     "r1",
+		Section:     "default",
+		Homepage:    "http://carlosbecker.com",
+		Vendor:      "nope",
+		Overridables: nfpm.Overridables{
+			Depends: []string{
+				"bash",
+				"foo",
+			},
+			Recommends: []string{
+				"git",
+				"bar",
+			},
+			Suggests: []string{
+				"bash",
+				"lala",
+			},
+			Replaces: []string{
+				"svn",
+				"subversion",
+			},
+			Provides: []string{
+				"bzr",
+				"zzz",
+			},
+			Conflicts: []string{
+				"zsh",
+				"foobarsh",
+			},
+			Files: map[string]string{
+				"../testdata/fake":          "/usr/local/bin/fake",
+				"../testdata/whatever.conf": "/usr/share/doc/fake/fake.txt",
+			},
+			ConfigFiles: map[string]string{
+				"../testdata/whatever.conf": "/etc/fake/fake.conf",
+			},
+			EmptyFolders: []string{
+				"/var/log/whatever",
+				"/usr/share/whatever",
+			},
+		},
+	})
+}
+
+func TestArchToAlpine(t *testing.T) {
+	verifyArch(t, "", "")
+	verifyArch(t, "abc", "abc")
+	verifyArch(t, "386", "x86")
+	verifyArch(t, "amd64", "x86_64")
+	verifyArch(t, "arm", "armhf")
+	verifyArch(t, "arm6", "armhf")
+	verifyArch(t, "arm7", "armhf")
+	verifyArch(t, "arm64", "aarch64")
+}
+
+func verifyArch(t *testing.T, nfpmArch, expectedArch string) {
+	info := exampleInfo()
+	info.Arch = nfpmArch
+
+	assert.NoError(t, Default.Package(info, ioutil.Discard))
+	assert.Equal(t, expectedArch, info.Arch)
+}
+
+func TestCreateBuilderData(t *testing.T) {
+	info := exampleInfo()
+	size := int64(0)
+	builderData := createBuilderData(info, &size)
+
+	var buf bytes.Buffer
+	tw := tar.NewWriter(&buf)
+
+	assert.NoError(t, builderData(tw))
+
+	assert.Equal(t, 8712, buf.Len())
+}
+
+func TestCombineToApk(t *testing.T) {
+	var bufData bytes.Buffer
+	bufData.Write([]byte{1})
+
+	var bufControl bytes.Buffer
+	bufControl.Write([]byte{2})
+
+	var bufTarget bytes.Buffer
+
+	assert.NoError(t, combineToApk(&bufTarget, &bufData, &bufControl))
+	assert.Equal(t, 2, bufTarget.Len())
+}
+
+func TestPathsToCreate(t *testing.T) {
+	for pathToTest, parts := range map[string][]string{
+		"/usr/share/doc/whatever/foo.md": {"usr", "usr/share", "usr/share/doc", "usr/share/doc/whatever"},
+		"/var/moises":                    {"var"},
+		"/":                              []string(nil),
+	} {
+		parts := parts
+		pathToTest := pathToTest
+		t.Run(fmt.Sprintf("pathToTest: '%s'", pathToTest), func(t *testing.T) {
+			assert.Equal(t, parts, pathsToCreate(pathToTest))
+		})
+	}
+}
+
+func TestDefaultWithArch(t *testing.T) {
+	for _, arch := range []string{"386", "amd64"} {
+		arch := arch
+		t.Run(arch, func(t *testing.T) {
+			info := exampleInfo()
+			info.Arch = arch
+			var err = Default.Package(info, ioutil.Discard)
+			assert.NoError(t, err)
+		})
+	}
+}
+
+func TestNoInfo(t *testing.T) {
+	var err = Default.Package(nfpm.WithDefaults(&nfpm.Info{}), ioutil.Discard)
+	assert.NoError(t, err)
+}
+
+func TestFileDoesNotExist(t *testing.T) {
+	var err = Default.Package(
+		nfpm.WithDefaults(&nfpm.Info{
+			Name:        "foo",
+			Arch:        "amd64",
+			Description: "Foo does things",
+			Priority:    "extra",
+			Maintainer:  "Carlos A Becker <pkg@carlosbecker.com>",
+			Version:     "1.0.0",
+			Section:     "default",
+			Homepage:    "http://carlosbecker.com",
+			Vendor:      "nope",
+			Overridables: nfpm.Overridables{
+				Depends: []string{
+					"bash",
+				},
+				Files: map[string]string{
+					"../testdata/": "/usr/local/bin/fake",
+				},
+				ConfigFiles: map[string]string{
+					"../testdata/whatever.confzzz": "/etc/fake/fake.conf",
+				},
+			},
+		}),
+		ioutil.Discard,
+	)
+	assert.EqualError(t, err, "../testdata/whatever.confzzz: file does not exist")
+}
+
+func TestNoFiles(t *testing.T) {
+	var err = Default.Package(
+		nfpm.WithDefaults(&nfpm.Info{
+			Name:        "foo",
+			Arch:        "amd64",
+			Description: "Foo does things",
+			Priority:    "extra",
+			Maintainer:  "Carlos A Becker <pkg@carlosbecker.com>",
+			Version:     "1.0.0",
+			Section:     "default",
+			Homepage:    "http://carlosbecker.com",
+			Vendor:      "nope",
+			Overridables: nfpm.Overridables{
+				Depends: []string{
+					"bash",
+				},
+			},
+		}),
+		ioutil.Discard,
+	)
+	assert.NoError(t, err)
+}
+
+func TestCreateBuilderControl(t *testing.T) {
+	info := exampleInfo()
+	size := int64(12345)
+	builderControl := createBuilderControl(info, size)
+
+	var w bytes.Buffer
+	tw := tar.NewWriter(&w)
+	assert.NoError(t, builderControl(tw))
+
+	var control = extractControl(t, &w)
+	var golden = "testdata/TestCreateBuilderControl.golden"
+	if *update {
+		require.NoError(t, ioutil.WriteFile(golden, []byte(control), 0655)) // nolint: gosec
+	}
+	bts, err := ioutil.ReadFile(golden) //nolint:gosec
+	assert.NoError(t, err)
+	assert.Equal(t, string(bts), control)
+}
+
+func TestCreateBuilderControlScripts(t *testing.T) {
+	info := exampleInfo()
+	info.Scripts = nfpm.Scripts{
+		PreInstall:  "../testdata/scripts/preinstall.sh",
+		PostInstall: "../testdata/scripts/postinstall.sh",
+		PreRemove:   "../testdata/scripts/preremove.sh",
+		PostRemove:  "../testdata/scripts/postremove.sh",
+	}
+
+	size := int64(12345)
+	builderControl := createBuilderControl(info, size)
+
+	var w bytes.Buffer
+	tw := tar.NewWriter(&w)
+	assert.NoError(t, builderControl(tw))
+
+	var control = extractControl(t, &w)
+	var golden = "testdata/TestCreateBuilderControlScripts.golden"
+	if *update {
+		require.NoError(t, ioutil.WriteFile(golden, []byte(control), 0655)) // nolint: gosec
+	}
+	bts, err := ioutil.ReadFile(golden) //nolint:gosec
+	assert.NoError(t, err)
+	assert.Equal(t, string(bts), control)
+}
+
+func TestControl(t *testing.T) {
+	var w bytes.Buffer
+	assert.NoError(t, writeControl(&w, controlData{
+		Info:          exampleInfo(),
+		InstalledSize: 10,
+	}))
+	var golden = "testdata/TestControl.golden"
+	if *update {
+		require.NoError(t, ioutil.WriteFile(golden, w.Bytes(), 0655)) // nolint: gosec
+	}
+	bts, err := ioutil.ReadFile(golden) //nolint:gosec
+	assert.NoError(t, err)
+	assert.Equal(t, string(bts), w.String())
+}
+
+func extractControl(t *testing.T, r io.Reader) string {
+	var tr = tar.NewReader(r)
+
+	for {
+		hdr, err := tr.Next()
+		if err == io.EOF {
+			break
+		}
+		require.NoError(t, err)
+		if hdr.Name == ".PKGINFO" {
+			var w bytes.Buffer
+			_, err := io.Copy(&w, tr)
+			require.NoError(t, err)
+			return w.String()
+		}
+		t.Log("ignored", hdr.Name)
+	}
+
+	return ""
+}
diff --git a/apk/testdata/TestControl.golden b/apk/testdata/TestControl.golden
new file mode 100644
index 0000000..4cb4328
--- /dev/null
+++ b/apk/testdata/TestControl.golden
@@ -0,0 +1,13 @@
+pkgname = foo
+pkgver = 1.0.0-r1
+arch = amd64
+size = 10
+pkgdesc = Foo does things
+url = http://carlosbecker.com
+maintainer = Carlos A Becker <pkg@carlosbecker.com>
+replaces = svn
+replaces = subversion
+provides = bzr
+provides = zzz
+depend = bash
+depend = foo
diff --git a/apk/testdata/TestCreateBuilderControl.golden b/apk/testdata/TestCreateBuilderControl.golden
new file mode 100644
index 0000000..d1a4f20
--- /dev/null
+++ b/apk/testdata/TestCreateBuilderControl.golden
@@ -0,0 +1,13 @@
+pkgname = foo
+pkgver = 1.0.0-r1
+arch = amd64
+size = 12345
+pkgdesc = Foo does things
+url = http://carlosbecker.com
+maintainer = Carlos A Becker <pkg@carlosbecker.com>
+replaces = svn
+replaces = subversion
+provides = bzr
+provides = zzz
+depend = bash
+depend = foo
diff --git a/apk/testdata/TestCreateBuilderControlScripts.golden b/apk/testdata/TestCreateBuilderControlScripts.golden
new file mode 100644
index 0000000..d1a4f20
--- /dev/null
+++ b/apk/testdata/TestCreateBuilderControlScripts.golden
@@ -0,0 +1,13 @@
+pkgname = foo
+pkgver = 1.0.0-r1
+arch = amd64
+size = 12345
+pkgdesc = Foo does things
+url = http://carlosbecker.com
+maintainer = Carlos A Becker <pkg@carlosbecker.com>
+replaces = svn
+replaces = subversion
+provides = bzr
+provides = zzz
+depend = bash
+depend = foo
diff --git a/nfpm_test.go b/nfpm_test.go
index bf577d2..4fc0877 100644
--- a/nfpm_test.go
+++ b/nfpm_test.go
@@ -139,6 +139,7 @@ func TestParseFile(t *testing.T) {
 	require.Error(t, err)
 	Register("deb", &fakePackager{})
 	Register("rpm", &fakePackager{})
+	Register("apk", &fakePackager{})
 	_, err = ParseFile("./testdata/overrides.yaml")
 	require.NoError(t, err)
 	_, err = ParseFile("./testdata/doesnotexist.yaml")
diff --git a/testdata/overrides.yaml b/testdata/overrides.yaml
index 3c6ff18..84182cf 100644
--- a/testdata/overrides.yaml
+++ b/testdata/overrides.yaml
@@ -17,3 +17,8 @@ overrides:
       - rpm_depend
     config_files:
       rpm.conf: "/rpm/path"
+  apk:
+    depends:
+      - apk_depend
+    config_files:
+      apk.conf: "/apk/path"