Repository: chainguard-dev/apko
Pull request #1606 (2025-04-08 15:14:26)

Consider deps resolved if other resolved deps already provide them


Patch:
diff --git a/go.mod b/go.mod
index 1d9a02d..51c6ced 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,6 @@ require (
 	github.com/klauspost/compress v1.18.0
 	github.com/klauspost/pgzip v1.2.6
 	github.com/package-url/packageurl-go v0.1.3
-	github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e
 	github.com/sigstore/cosign/v2 v2.4.3
 	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
 	github.com/spf13/cobra v1.9.1
diff --git a/go.sum b/go.sum
index 37c8a44..380c282 100644
--- a/go.sum
+++ b/go.sum
@@ -160,7 +160,6 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -282,8 +281,6 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq
 github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e h1:51xcRlSMBU5rhM9KahnJGfEsBPVPz3182TgFRowA8yY=
-github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
diff --git a/pkg/apk/tarball/testdata/bar/bar.txt b/internal/cli/testdata/bar/bar.txt
similarity index 100%
rename from pkg/apk/tarball/testdata/bar/bar.txt
rename to internal/cli/testdata/bar/bar.txt
diff --git a/pkg/apk/tarball/testdata/foo/foo.txt b/internal/cli/testdata/foo/foo.txt
similarity index 100%
rename from pkg/apk/tarball/testdata/foo/foo.txt
rename to internal/cli/testdata/foo/foo.txt
diff --git a/pkg/apk/README.md b/pkg/apk/README.md
index 0c25206..03c62fc 100644
--- a/pkg/apk/README.md
+++ b/pkg/apk/README.md
@@ -9,7 +9,6 @@ Also includes supporting utilities for working with filesystems, including:
 read-write, chmod/chown, devices, and symlinks capabilities
 * an implementation of that FS in memory, i.e. a memfs
 * an implementation of that FS on top of a directory, which uses the memfs for features the underlying disk does not support
-* tarball features
 
 Documentation is available at [https://pkg.go.dev/chainguard.dev/apko/pkg/apk](https://pkg.go.dev/chainguard.dev/apko/pkg/apk).
 
@@ -59,11 +58,6 @@ It also provides two implementations of that interface:
 * `memfs` is an in-memory implementation of `FullFS`. It is fully functional, but remember that it uses memory, so loading very large files into it will hit limits.
 * `rwosfs` is an on-disk implementation of `FullFS`. It is fully functional, including capabilities that may not exist on the underlying filesystem, like symlinks, devices, chown/chmod and case-sensitivity. The metadata for every file on disk also is in-memory, enabling those additional capabilities. Contents are not stored in memory.
 
-### Tarball
-
-`chainguard.dev/apko/pkg/apk/tarball` provides a utility to write an [fs.FS](https://pkg.go.dev/io/fs#FS) to a tarball. It is implemented on a `tarball.Context`, which lets
-you provide overrides for timestamps, UID/GID, and other features.
-
 ### apk
 
 `chainguard.dev/apko/pkg/apk/apk` is the heart of this library. It provides a native go
diff --git a/pkg/apk/apk/implementation.go b/pkg/apk/apk/implementation.go
index 763fc68..1e6859a 100644
--- a/pkg/apk/apk/implementation.go
+++ b/pkg/apk/apk/implementation.go
@@ -1211,7 +1211,7 @@ func expandPackage(ctx context.Context, a *APK, pkg InstallablePackage) (*expand
 	return a.cachePackage(ctx, pkg, exp, cacheDir)
 }
 
-func packageAsURI(pkg InstallablePackage) (uri.URI, error) {
+func packageAsURI(pkg LocatablePackage) (uri.URI, error) {
 	u := pkg.URL()
 
 	if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
@@ -1221,7 +1221,7 @@ func packageAsURI(pkg InstallablePackage) (uri.URI, error) {
 	return uri.New(u), nil
 }
 
-func packageAsURL(pkg InstallablePackage) (*url.URL, error) {
+func packageAsURL(pkg LocatablePackage) (*url.URL, error) {
 	asURI, err := packageAsURI(pkg)
 	if err != nil {
 		return nil, err
@@ -1230,7 +1230,7 @@ func packageAsURL(pkg InstallablePackage) (*url.URL, error) {
 	return url.Parse(string(asURI))
 }
 
-func (a *APK) FetchPackage(ctx context.Context, pkg InstallablePackage) (io.ReadCloser, error) {
+func (a *APK) FetchPackage(ctx context.Context, pkg FetchablePackage) (io.ReadCloser, error) {
 	log := clog.FromContext(ctx)
 	log.Debugf("fetching %s", pkg)
 
diff --git a/pkg/apk/apk/index.go b/pkg/apk/apk/index.go
index 94d6534..27f8470 100644
--- a/pkg/apk/apk/index.go
+++ b/pkg/apk/apk/index.go
@@ -428,11 +428,11 @@ func parseRepositoryIndex(ctx context.Context, u string, keys map[string][]byte,
 		for _, sig := range sigs {
 			// compute the digest if not already done
 			if _, hasDigest := indexDigest[sig.DigestAlgorithm]; !hasDigest {
-				digest, err := sign.HashData(indexData, sig.DigestAlgorithm)
-				if err != nil {
-					return nil, fmt.Errorf("failed to compute digest: %w", err)
+				h := sig.DigestAlgorithm.New()
+				if n, err := h.Write(indexData); err != nil || n != len(indexData) {
+					return nil, fmt.Errorf("unable to hash data: %w", err)
 				}
-				indexDigest[sig.DigestAlgorithm] = digest
+				indexDigest[sig.DigestAlgorithm] = h.Sum(nil)
 			}
 			if err := sign.RSAVerifyDigest(indexDigest[sig.DigestAlgorithm], sig.DigestAlgorithm, sig.Signature, keys[sig.KeyID]); err == nil {
 				verified = true
diff --git a/pkg/apk/apk/package.go b/pkg/apk/apk/package.go
index 99251f6..2603958 100644
--- a/pkg/apk/apk/package.go
+++ b/pkg/apk/apk/package.go
@@ -60,6 +60,40 @@ func PackageToInstalled(pkg *Package) (out []string) {
 	return
 }
 
+// LocatablePackage represents a minimal set of information needed to locate a
+// package for retrieval over a network.
+type LocatablePackage interface {
+	URL() string
+}
+
+// FetchablePackage represents a minimal set of information needed to fetch a package.
+type FetchablePackage interface {
+	URL() string
+	PackageName() string
+}
+
+type fetchablePackage struct {
+	pkgName string
+	url     string
+}
+
+func (f fetchablePackage) URL() string {
+	return f.url
+}
+
+func (f fetchablePackage) PackageName() string {
+	return f.pkgName
+}
+
+// NewFetchablePackage creates and returns a minimal implementation of
+// FetchablePackage.
+func NewFetchablePackage(pkgName, url string) FetchablePackage {
+	return &fetchablePackage{
+		pkgName: pkgName,
+		url:     url,
+	}
+}
+
 // InstallablePackage represents a minimal set of information needed to install a package within an Image.
 type InstallablePackage interface {
 	URL() string
diff --git a/pkg/apk/apk/repo.go b/pkg/apk/apk/repo.go
index 9da904f..b46679f 100644
--- a/pkg/apk/apk/repo.go
+++ b/pkg/apk/apk/repo.go
@@ -787,6 +787,33 @@ func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg *Repositor
 					return nil, nil, err
 				}
 
+				// See if any virtual packages of packages we've already
+				// selected satisfy this contraint.
+				satisfiedByProvide := false
+				for _, provide := range picked.Provides {
+					prostraint := cachedResolvePackageNameVersionPin(provide)
+					pname, pversion, pcompare := prostraint.name, prostraint.version, prostraint.dep
+					if pname != name {
+						continue
+					}
+					// If this virtual package is unversioned, it can't
+					// satisfy a versioned constraint
+					if pversion == "" {
+						continue
+					}
+					prover, err := cachedParseVersion(pversion)
+					if err != nil {
+						return nil, nil, err
+					}
+					if pcompare.satisfies(prover, requiredVersion) {
+						satisfiedByProvide = true
+						break
+					}
+				}
+				if satisfiedByProvide {
+					continue
+				}
+
 				// We do care which version and they match.
 				if compare.satisfies(actualVersion, requiredVersion) {
 					continue
diff --git a/pkg/apk/signature/sign.go b/pkg/apk/signature/sign.go
deleted file mode 100644
index 732b9ef..0000000
--- a/pkg/apk/signature/sign.go
+++ /dev/null
@@ -1,145 +0,0 @@
-// Copyright 2023 Chainguard, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package signature
-
-import (
-	"archive/tar"
-	"bytes"
-	"context"
-	"crypto"
-	"errors"
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-	"strings"
-
-	"github.com/chainguard-dev/clog"
-	"github.com/klauspost/compress/gzip"
-	"github.com/psanford/memfs"
-
-	"chainguard.dev/apko/pkg/apk/tarball"
-)
-
-func SignIndex(ctx context.Context, signingKey string, indexFile string) error {
-	log := clog.FromContext(ctx)
-	is, err := indexIsAlreadySigned(indexFile)
-	if err != nil {
-		return err
-	}
-	if is {
-		log.Infof("index %s is already signed, doing nothing", indexFile)
-		return nil
-	}
-
-	log.Infof("signing index %s with key %s", indexFile, signingKey)
-
-	indexData, err := os.ReadFile(indexFile)
-	if err != nil {
-		return fmt.Errorf("unable to read index for signing: %w", err)
-	}
-
-	sigFS := memfs.New()
-	indexDigest, err := HashData(indexData, crypto.SHA256)
-	if err != nil {
-		return err
-	}
-
-	sigData, err := RSASignDigest(indexDigest, crypto.SHA256, signingKey, "")
-	if err != nil {
-		return fmt.Errorf("unable to sign index: %w", err)
-	}
-
-	log.Infof("appending signature RSA256 to index %s", indexFile)
-
-	if err := sigFS.WriteFile(fmt.Sprintf(".SIGN.RSA256.%s.pub", filepath.Base(signingKey)), sigData, 0644); err != nil {
-		return fmt.Errorf("unable to append signature: %w", err)
-	}
-
-	// prepare control.tar.gz
-	multitarctx, err := tarball.NewContext(
-		tarball.WithOverrideUIDGID(0, 0),
-		tarball.WithOverrideUname("root"),
-		tarball.WithOverrideGname("root"),
-		tarball.WithSkipClose(true),
-	)
-	if err != nil {
-		return fmt.Errorf("unable to build tarball context: %w", err)
-	}
-
-	log.Infof("writing signed index to %s", indexFile)
-
-	var sigBuffer bytes.Buffer
-	if err := multitarctx.WriteTargz(ctx, &sigBuffer, sigFS, sigFS); err != nil {
-		return fmt.Errorf("unable to write signature tarball: %w", err)
-	}
-
-	idx, err := os.Create(indexFile)
-	if err != nil {
-		return fmt.Errorf("unable to open index for writing: %w", err)
-	}
-	defer idx.Close()
-
-	if _, err := io.Copy(idx, &sigBuffer); err != nil {
-		return fmt.Errorf("unable to write index signature: %w", err)
-	}
-
-	if _, err := idx.Write(indexData); err != nil {
-		return fmt.Errorf("unable to write index data: %w", err)
-	}
-
-	log.Infof("signed index %s with key %s", indexFile, signingKey)
-
-	return nil
-}
-
-func indexIsAlreadySigned(indexFile string) (bool, error) {
-	index, err := os.Open(indexFile)
-	if err != nil {
-		return false, fmt.Errorf("cannot open index file %s: %w", indexFile, err)
-	}
-	defer index.Close()
-
-	gzi, err := gzip.NewReader(index)
-	if err != nil {
-		return false, fmt.Errorf("cannot open index file %s as gzip: %w", indexFile, err)
-	}
-	defer gzi.Close()
-
-	tari := tar.NewReader(gzi)
-	for {
-		hdr, err := tari.Next()
-		if errors.Is(err, io.EOF) {
-			break
-		}
-		if err != nil {
-			return false, fmt.Errorf("cannot read tar index %s: %w", indexFile, err)
-		}
-
-		if strings.HasPrefix(hdr.Name, ".SIGN.RSA") {
-			return true, nil
-		}
-	}
-
-	return false, nil
-}
-
-func HashData(data []byte, digestType crypto.Hash) ([]byte, error) {
-	digest := digestType.New()
-	if n, err := digest.Write(data); err != nil || n != len(data) {
-		return nil, fmt.Errorf("unable to hash data: %w", err)
-	}
-	return digest.Sum(nil), nil
-}
diff --git a/pkg/apk/tarball/tarball.go b/pkg/apk/tarball/tarball.go
deleted file mode 100644
index 2c71dad..0000000
--- a/pkg/apk/tarball/tarball.go
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright 2022, 2023 Chainguard, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tarball
-
-import (
-	"archive/tar"
-	"time"
-)
-
-type Context struct {
-	SourceDateEpoch    time.Time
-	useSourceDateEpoch bool
-
-	OverrideUIDGID bool
-	UID            int
-	GID            int
-	OverrideUname  string
-	OverrideGname  string
-	SkipClose      bool
-	UseChecksums   bool
-	remapUIDs      map[int]int
-	remapGIDs      map[int]int
-	overridePerms  map[string]tar.Header
-}
-
-type Option func(*Context) error
-
-// Generates a new context from a set of options.
-func NewContext(opts ...Option) (*Context, error) {
-	ctx := Context{}
-
-	for _, opt := range opts {
-		if err := opt(&ctx); err != nil {
-			return nil, err
-		}
-	}
-
-	return &ctx, nil
-}
-
-// Sets SourceDateEpoch for Context.
-func WithSourceDateEpoch(t time.Time) Option {
-	return func(ctx *Context) error {
-		ctx.SourceDateEpoch = t
-
-		// to distinguish between unset and zero value
-		ctx.useSourceDateEpoch = true
-
-		return nil
-	}
-}
-
-// WithOverrideUIDGID sets the UID/GID to override with for all files for Context.
-func WithOverrideUIDGID(uid, gid int) Option {
-	return func(ctx *Context) error {
-		ctx.OverrideUIDGID = true
-		ctx.UID = uid
-		ctx.GID = gid
-		return nil
-	}
-}
-
-// WithOverridePerms sets the UID/GID and file permissions to override with for specific files Context.
-func WithOverridePerms(files []tar.Header) Option {
-	return func(ctx *Context) error {
-		overrides := map[string]tar.Header{}
-		for _, f := range files {
-			overrides[f.Name] = f
-		}
-		ctx.overridePerms = overrides
-		return nil
-	}
-}
-
-// WithOverrideUname sets the Uname to use with Context.
-func WithOverrideUname(uname string) Option {
-	return func(ctx *Context) error {
-		ctx.OverrideUname = uname
-		return nil
-	}
-}
-
-// WithRemapUIDs sets a UID remapping in the Context.
-func WithRemapUIDs(uids map[int]int) Option {
-	return func(ctx *Context) error {
-		ctx.remapUIDs = uids
-		return nil
-	}
-}
-
-// WithRemapGIDs sets a GID remapping in the Context.
-func WithRemapGIDs(gids map[int]int) Option {
-	return func(ctx *Context) error {
-		ctx.remapGIDs = gids
-		return nil
-	}
-}
-
-// WithOverrideGname sets the Gname to use with Context.
-func WithOverrideGname(gname string) Option {
-	return func(ctx *Context) error {
-		ctx.OverrideGname = gname
-		return nil
-	}
-}
-
-// WithSkipClose is used to determine whether the tar stream
-// should be closed.  For concatenated tar streams such as APKv2
-// containers, only the final tar stream should be closed.
-func WithSkipClose(skipClose bool) Option {
-	return func(ctx *Context) error {
-		ctx.SkipClose = skipClose
-		return nil
-	}
-}
-
-// WithUseChecksums is used to determine whether the tar stream
-// should have the APK-TOOLS.checksum.SHA1 extension.
-func WithUseChecksums(useChecksums bool) Option {
-	return func(ctx *Context) error {
-		ctx.UseChecksums = useChecksums
-		return nil
-	}
-}
diff --git a/pkg/apk/tarball/write.go b/pkg/apk/tarball/write.go
deleted file mode 100644
index 6a89632..0000000
--- a/pkg/apk/tarball/write.go
+++ /dev/null
@@ -1,343 +0,0 @@
-// Copyright 2022, 2023 Chainguard, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package tarball
-
-import (
-	"archive/tar"
-	"compress/gzip"
-	"context"
-	"crypto/sha1" //nolint:gosec
-	"encoding/hex"
-	"fmt"
-	"io"
-	"io/fs"
-	"os"
-	"syscall"
-
-	"go.opentelemetry.io/otel"
-	"golang.org/x/sys/unix"
-
-	apkfs "chainguard.dev/apko/pkg/apk/fs"
-	"chainguard.dev/apko/pkg/apk/passwd"
-)
-
-const xattrTarPAXRecordsPrefix = "SCHILY.xattr."
-
-func hasHardlinks(fi fs.FileInfo) bool {
-	if stat := fi.Sys(); stat != nil {
-		si, ok := stat.(*syscall.Stat_t)
-		if !ok {
-			return false
-		}
-
-		// if we don't have inodes, we just assume the filesystem
-		// does not support hardlinks
-		if si == nil {
-			return false
-		}
-
-		return si.Nlink > 1
-	}
-
-	return false
-}
-
-func getInodeFromFileInfo(fi fs.FileInfo) (uint64, error) {
-	if stat := fi.Sys(); stat != nil {
-		si, ok := stat.(*syscall.Stat_t)
-		if !ok {
-			return 0, fmt.Errorf("unable to stat underlying file")
-		}
-
-		// if we don't have inodes, we just assume the filesystem
-		// does not support hardlinks
-		if si == nil {
-			return 0, fmt.Errorf("unable to stat underlying file")
-		}
-
-		return si.Ino, nil
-	}
-
-	return 0, fmt.Errorf("unable to stat underlying file")
-}
-
-func (c *Context) writeTar(ctx context.Context, tw *tar.Writer, fsys fs.FS, users, groups map[int]string) error { //nolint:gocyclo
-	if users == nil {
-		users = map[int]string{}
-	}
-	if groups == nil {
-		groups = map[int]string{}
-	}
-	seenFiles := map[uint64]string{}
-	// set this once, to make it easy to look up later
-	if c.overridePerms == nil {
-		c.overridePerms = map[string]tar.Header{}
-	}
-
-	buf := make([]byte, 1<<20)
-
-	if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
-		if err := ctx.Err(); err != nil {
-			return err
-		}
-		// skip the root path, superfluous
-		if path == "." {
-			return nil
-		}
-
-		if err != nil {
-			return err
-		}
-
-		info, err := d.Info()
-		if err != nil {
-			return err
-		}
-
-		var (
-			link         string
-			major, minor uint32
-			isCharDevice bool
-		)
-		if info.Mode()&os.ModeSymlink == os.ModeSymlink {
-			rlfs, ok := fsys.(apkfs.ReadLinkFS)
-			if !ok {
-				return fmt.Errorf("readlink not supported by this fs: path (%s)", path)
-			}
-
-			if link, err = rlfs.Readlink(path); err != nil {
-				return err
-			}
-		}
-
-		if info.Mode()&os.ModeCharDevice == os.ModeCharDevice {
-			rlfs, ok := fsys.(apkfs.ReadnodFS)
-			if !ok {
-				return fmt.Errorf("read character device not supported by this fs: path (%s) %#v %#v", path, info, fsys)
-			}
-			isCharDevice = true
-			dev, err := rlfs.Readnod(path)
-			if err != nil {
-				return err
-			}
-			major = unix.Major(uint64(dev))
-			minor = unix.Minor(uint64(dev))
-		}
-
-		header, err := tar.FileInfoHeader(info, link)
-		if err != nil {
-			return err
-		}
-		// devices
-		if isCharDevice {
-			header.Devmajor = int64(major)
-			header.Devminor = int64(minor)
-		}
-		// work around some weirdness, without this we wind up with just the basename
-		header.Name = path
-
-		// if SourceDateEpoch is set explicitly, overwrite the timestamps, otherwise propagate modtime
-		// (this option is unused in apko but melange uses this, so we support it for now)
-		if c.useSourceDateEpoch {
-			header.AccessTime = c.SourceDateEpoch
-			header.ModTime = c.SourceDateEpoch
-			header.ChangeTime = c.SourceDateEpoch
-		} else {
-			header.ModTime = info.ModTime()
-		}
-
-		if uid, ok := c.remapUIDs[header.Uid]; ok {
-			header.Uid = uid
-		}
-
-		if gid, ok := c.remapGIDs[header.Gid]; ok {
-			header.Gid = gid
-		}
-
-		if name, ok := users[header.Uid]; ok {
-			header.Uname = name
-		}
-		if name, ok := groups[header.Gid]; ok {
-			header.Gname = name
-		}
-
-		if c.OverrideUIDGID {
-			header.Uid = c.UID
-			header.Gid = c.GID
-		}
-
-		if c.OverrideUname != "" {
-			header.Uname = c.OverrideUname
-		}
-
-		if c.OverrideGname != "" {
-			header.Gname = c.OverrideGname
-		}
-
-		// look for the override perms with or without the leading /
-		if h, ok := c.overridePerms[header.Name]; ok {
-			header.Mode = h.Mode
-			header.Uid = h.Uid
-			header.Gid = h.Gid
-			header.Uname = h.Uname
-			header.Gname = h.Gname
-		}
-		if h, ok := c.overridePerms["/"+header.Name]; ok {
-			header.Mode = h.Mode
-			header.Uid = h.Uid
-			header.Gid = h.Gid
-			header.Uname = h.Uname
-			header.Gname = h.Gname
-		}
-
-		if link != "" {
-			header.Typeflag = tar.TypeSymlink
-		}
-		if !info.IsDir() && hasHardlinks(info) {
-			inode, err := getInodeFromFileInfo(info)
-			if err != nil {
-				return err
-			}
-
-			if oldpath, ok := seenFiles[inode]; ok {
-				header.Typeflag = tar.TypeLink
-				header.Linkname = oldpath
-				header.Size = 0
-			} else {
-				seenFiles[inode] = header.Name
-			}
-		}
-
-		if header.PAXRecords == nil {
-			header.PAXRecords = map[string]string{}
-		}
-		if c.UseChecksums {
-			if link != "" {
-				linkDigest := sha1.Sum([]byte(link)) //nolint:gosec
-				linkChecksum := hex.EncodeToString(linkDigest[:])
-				header.PAXRecords["APK-TOOLS.checksum.SHA1"] = linkChecksum
-			} else if info.Mode().IsRegular() {
-				data, err := fsys.Open(path)
-				if err != nil {
-					return err
-				}
-				defer data.Close()
-
-				fileDigest := sha1.New() //nolint:gosec
-				if _, err := io.CopyBuffer(fileDigest, data, buf); err != nil {
-					return err
-				}
-
-				fileChecksum := hex.EncodeToString(fileDigest.Sum(nil))
-				header.PAXRecords["APK-TOOLS.checksum.SHA1"] = fileChecksum
-			}
-		}
-
-		// only capture xattrs for real objects in the FS
-		if header.Typeflag == tar.TypeReg || header.Typeflag == tar.TypeDir {
-			xfs, ok := fsys.(apkfs.XattrFS)
-			if ok {
-				xattrs, err := xfs.ListXattrs(path)
-				// we can ignore errors
-				if err == nil && xattrs != nil {
-					for name, value := range xattrs {
-						header.PAXRecords[xattrTarPAXRecordsPrefix+name] = string(value)
-					}
-				}
-			}
-		}
-
-		if err := tw.WriteHeader(header); err != nil {
-			return err
-		}
-
-		if info.Mode().IsRegular() && header.Size > 0 {
-			data, err := fsys.Open(path)
-			if err != nil {
-				return err
-			}
-
-			defer data.Close()
-
-			if _, err := io.CopyBuffer(tw, data, buf); err != nil {
-				return err
-			}
-		}
-
-		return nil
-	}); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// WriteArchive writes a tarball to the provided io.Writer from the provided fs.FS.
-// To override permissions, set the OverridePerms when creating the Context.
-// If you need to get multiple filesystems, merge them prior to calling WriteArchive.
-//
-// Deprecated: Use WriteTargz or WriteTar instead.
-func (c *Context) WriteArchive(dst io.Writer, src fs.FS) error {
-	return c.WriteTargz(context.Background(), dst, src, src)
-}
-
-// WriteTargz writes a gzipped tarball to the provided io.Writer from the provided fs.FS.
-// To override permissions, set the OverridePerms when creating the Context.
-// If you need to get multiple filesystems, merge them prior to calling WriteArchive.
-// userinfosrc should be a fs which can provide an optionally provide an etc/passwd and etc/group file.
-// The etc/passwd and etc/group file provide username and group name mappings for the tar.
-func (c *Context) WriteTargz(ctx context.Context, dst io.Writer, src fs.FS, userinfofs fs.FS) error {
-	ctx, span := otel.Tracer("go-apk").Start(ctx, "WriteTargz")
-	defer span.End()
-
-	gzw := gzip.NewWriter(dst)
-	defer gzw.Close()
-
-	return c.WriteTar(ctx, gzw, src, userinfofs)
-}
-
-// WriteTar writes a tarball to the provided io.Writer from the provided fs.FS.
-// To override permissions, set the OverridePerms when creating the Context.
-// If you need to get multiple filesystems, merge them prior to calling WriteArchive.
-// userinfosrc should be a fs which can provide an optionally provide an etc/passwd and etc/group file.
-// The etc/passwd and etc/group file provide username and group name mappings for the tar.
-func (c *Context) WriteTar(ctx context.Context, dst io.Writer, src fs.FS, userinfosrc fs.FS) error {
-	ctx, span := otel.Tracer("go-apk").Start(ctx, "WriteTar")
-	defer span.End()
-
-	tw := tar.NewWriter(dst)
-	if !c.SkipClose {
-		defer tw.Close()
-	} else {
-		defer tw.Flush()
-	}
-
-	// get the uname and gname maps
-	usersFile, _ := passwd.ReadUserFile(userinfosrc, "etc/passwd")
-	groupsFile, _ := passwd.ReadGroupFile(userinfosrc, "etc/group")
-	users := map[int]string{}
-	groups := map[int]string{}
-	for _, u := range usersFile.Entries {
-		users[int(u.UID)] = u.UserName
-	}
-	for _, g := range groupsFile.Entries {
-		groups[int(g.GID)] = g.GroupName
-	}
-	if err := c.writeTar(ctx, tw, src, users, groups); err != nil {
-		return fmt.Errorf("writing TAR archive failed: %w", err)
-	}
-
-	return nil
-}
diff --git a/pkg/build/build_implementation.go b/pkg/build/build_implementation.go
index 7d781f8..80c85fc 100644
--- a/pkg/build/build_implementation.go
+++ b/pkg/build/build_implementation.go
@@ -34,7 +34,6 @@ import (
 	"github.com/chainguard-dev/clog"
 
 	"chainguard.dev/apko/pkg/apk/apk"
-	"chainguard.dev/apko/pkg/apk/tarball"
 	"chainguard.dev/apko/pkg/lock"
 	"chainguard.dev/apko/pkg/options"
 )
@@ -100,11 +99,6 @@ func (bc *Context) BuildTarball(ctx context.Context) (string, hash.Hash, hash.Ha
 	bc.o.TarballPath = outfile.Name()
 	defer outfile.Close()
 
-	tw, err := tarball.NewContext()
-	if err != nil {
-		return "", nil, nil, 0, fmt.Errorf("failed to construct tarball build context: %w", err)
-	}
-
 	digest := sha256.New()
 
 	buf := pooledBufioWriter(outfile)
@@ -115,7 +109,7 @@ func (bc *Context) BuildTarball(ctx context.Context) (string, hash.Hash, hash.Ha
 
 	diffid := sha256.New()
 
-	if err := tw.WriteTar(ctx, io.MultiWriter(diffid, gzw), bc.fs, bc.fs); err != nil {
+	if err := writeTar(ctx, io.MultiWriter(diffid, gzw), bc.fs); err != nil {
 		return "", nil, nil, 0, fmt.Errorf("failed to generate tarball for image: %w", err)
 	}
 	if err := gzw.Close(); err != nil {
diff --git a/pkg/build/tarball.go b/pkg/build/tarball.go
new file mode 100644
index 0000000..4d72195
--- /dev/null
+++ b/pkg/build/tarball.go
@@ -0,0 +1,161 @@
+// Copyright 2022, 2023 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+	"archive/tar"
+	"context" //nolint:gosec
+	"fmt"
+	"io"
+	"io/fs"
+	"os"
+
+	"go.opentelemetry.io/otel"
+	"golang.org/x/sys/unix"
+
+	apkfs "chainguard.dev/apko/pkg/apk/fs"
+	"chainguard.dev/apko/pkg/apk/passwd"
+)
+
+const xattrTarPAXRecordsPrefix = "SCHILY.xattr."
+
+// writeTar writes a tarball to the provided io.Writer from the provided fs.FS.
+// The etc/passwd and etc/group file provide username and group name mappings for the tar.
+func writeTar(ctx context.Context, dst io.Writer, fsys apkfs.FullFS) error { //nolint:gocyclo
+	ctx, span := otel.Tracer("go-apk").Start(ctx, "WriteTar")
+	defer span.End()
+
+	tw := tar.NewWriter(dst)
+
+	// get the uname and gname maps
+	usersFile, _ := passwd.ReadUserFile(fsys, "etc/passwd")
+	groupsFile, _ := passwd.ReadGroupFile(fsys, "etc/group")
+	users := map[int]string{}
+	groups := map[int]string{}
+	for _, u := range usersFile.Entries {
+		users[int(u.UID)] = u.UserName
+	}
+	for _, g := range groupsFile.Entries {
+		groups[int(g.GID)] = g.GroupName
+	}
+
+	buf := make([]byte, 1<<20)
+
+	if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
+		if err := ctx.Err(); err != nil {
+			return err
+		}
+		// skip the root path, superfluous
+		if path == "." {
+			return nil
+		}
+
+		if err != nil {
+			return err
+		}
+
+		info, err := d.Info()
+		if err != nil {
+			return err
+		}
+
+		var (
+			link         string
+			major, minor uint32
+			isCharDevice bool
+		)
+		if info.Mode()&os.ModeSymlink == os.ModeSymlink {
+			if link, err = fsys.Readlink(path); err != nil {
+				return err
+			}
+		}
+
+		if info.Mode()&os.ModeCharDevice == os.ModeCharDevice {
+			isCharDevice = true
+			dev, err := fsys.Readnod(path)
+			if err != nil {
+				return err
+			}
+			major = unix.Major(uint64(dev))
+			minor = unix.Minor(uint64(dev))
+		}
+
+		header, err := tar.FileInfoHeader(info, link)
+		if err != nil {
+			return err
+		}
+		// devices
+		if isCharDevice {
+			header.Devmajor = int64(major)
+			header.Devminor = int64(minor)
+		}
+		// work around some weirdness, without this we wind up with just the basename
+		header.Name = path
+
+		header.ModTime = info.ModTime()
+
+		if name, ok := users[header.Uid]; ok {
+			header.Uname = name
+		}
+		if name, ok := groups[header.Gid]; ok {
+			header.Gname = name
+		}
+
+		if link != "" {
+			header.Typeflag = tar.TypeSymlink
+		}
+
+		if header.PAXRecords == nil {
+			header.PAXRecords = map[string]string{}
+		}
+
+		// only capture xattrs for real objects in the FS
+		if header.Typeflag == tar.TypeReg || header.Typeflag == tar.TypeDir {
+			xattrs, err := fsys.ListXattrs(path)
+			// we can ignore errors
+			if err == nil && xattrs != nil {
+				for name, value := range xattrs {
+					header.PAXRecords[xattrTarPAXRecordsPrefix+name] = string(value)
+				}
+			}
+		}
+
+		if err := tw.WriteHeader(header); err != nil {
+			return err
+		}
+
+		if info.Mode().IsRegular() && header.Size > 0 {
+			data, err := fsys.Open(path)
+			if err != nil {
+				return err
+			}
+			defer data.Close()
+
+			if _, err := io.CopyBuffer(tw, data, buf); err != nil {
+				return err
+			}
+		}
+
+		return nil
+	}); err != nil {
+		return fmt.Errorf("writing tar archive: %w", err)
+	}
+
+	if err := tw.Close(); err != nil {
+		return fmt.Errorf("closing tar writer: %w", err)
+	}
+
+	return nil
+}


Test patch:
diff --git a/pkg/apk/apk/repo_test.go b/pkg/apk/apk/repo_test.go
index 146ae3b..f12fe52 100644
--- a/pkg/apk/apk/repo_test.go
+++ b/pkg/apk/apk/repo_test.go
@@ -870,6 +870,39 @@ func TestSameProvidedVersion(t *testing.T) {
 	}
 }
 
+// If a package has a provides with a different version than the real
+// package (here libcurl-openssl4=8.12.1-r1 provides libcurl-abi=8.12.1),
+// both should be allowed in the same DAG
+func TestDepWithProvidesOfDiffVersion(t *testing.T) {
+	providers := map[string][]string{
+		"libcurl-openssl4=8.12.1-r1": {
+			"libcurl-abi=8.12.1", "so:libcurl.so.4=4",
+		},
+	}
+	dependers := map[string][]string{
+		"foo=1":                      {}, // an extra dep level needed to trigger bug
+		"libcurl-openssl4=8.12.1-r1": {"foo"},
+		"curl=8.12.1-r1":             {"libcurl-abi=8.12.1", "so:libcurl.so.4"},
+		"curl-dev=8.12.1-r1":         {"so:libcurl.so.4"},
+	}
+
+	resolver := makeResolver(providers, dependers)
+	pkgs, _, err := resolver.GetPackagesWithDependencies(context.Background(), []string{"curl-dev", "curl"}, nil)
+	require.NoError(t, err)
+
+	wantPkgs := []string{
+		"foo-1.apk",
+		"libcurl-openssl4-8.12.1-r1.apk",
+		"curl-dev-8.12.1-r1.apk",
+		"curl-8.12.1-r1.apk",
+	}
+
+	require.Len(t, pkgs, len(wantPkgs))
+	for i, pkg := range pkgs {
+		require.Equal(t, pkg.Filename(), wantPkgs[i])
+	}
+}
+
 func TestHigherProvidedVersion(t *testing.T) {
 	providers := map[string][]string{
 		"ld-linux=2.38-r10": {"so:ld-linux-aarch64.so.1=1.1"},
diff --git a/pkg/apk/tarball/write_test.go b/pkg/build/tarball_test.go
similarity index 88%
rename from pkg/apk/tarball/write_test.go
rename to pkg/build/tarball_test.go
index b9ab275..ac97901 100644
--- a/pkg/apk/tarball/write_test.go
+++ b/pkg/build/tarball_test.go
@@ -1,4 +1,4 @@
-package tarball
+package build
 
 import (
 	"archive/tar"
@@ -28,12 +28,8 @@ func TestWriteTar(t *testing.T) {
 	require.NoError(t, err, "error setting xattr on %s", dir)
 	err = m.SetXattr(file, "user.file", []byte("bar"))
 	require.NoError(t, err, "error setting xattr on %s", file)
-	ctx := Context{}
-	tw := tar.NewWriter(&buf)
-	err = ctx.writeTar(context.TODO(), tw, m, nil, nil)
+	err = writeTar(context.Background(), &buf, m)
 	require.NoError(t, err, "error writing tar")
-	err = tw.Close()
-	require.NoError(t, err, "error closing tar writer")
 
 	// now should be able to read the tar and check the xattrs
 	tr := tar.NewReader(&buf)