Repository: Azure/dalec
Pull request #586: Properly pass through target platform
Author: login: cpuguy83
Created: 2025-03-27T19:25:18Z | Merged: 2025-04-03T20:12:28Z

This makes sure that the requested target platform is propagated everywhere it needs to be.

There are some room for improvements here.
For example, source packages really should not
depend on the target platform and we could get
away with using the native platform there.
These kinds of improvements will require some
code restructuring which is out of scope for this
change.



Diff:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 760594dac..ac5219461 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -142,6 +142,8 @@ jobs:
 
       - name: download deps
         run: go mod download
+      - name: Setup QEMU
+        run: docker run --rm --privileged tonistiigi/binfmt:latest --install all
       - name: Run integration tests
         run: |
           set -ex
diff --git a/frontend/build.go b/frontend/build.go
index 853e9218b..a31a634e4 100644
--- a/frontend/build.go
+++ b/frontend/build.go
@@ -3,7 +3,9 @@ package frontend
 import (
 	"bytes"
 	"context"
+	stderrors "errors"
 	"fmt"
+	"runtime"
 	"time"
 
 	"github.com/Azure/dalec"
@@ -115,9 +117,33 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 	return BuildWithPlatformFromUIClient(ctx, client, dc, f)
 }
 
+func getPanicStack() error {
+	stackBuf := make([]uintptr, 32)
+	n := runtime.Callers(4, stackBuf) // Skip 4 frames to exclude runtime.Callers, the current function, and defer internals
+	stackBuf = stackBuf[:n]
+	frames := runtime.CallersFrames(stackBuf)
+	var stackTrace string
+	for {
+		frame, more := frames.Next()
+		stackTrace += fmt.Sprintf("%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
+		if !more {
+			break
+		}
+	}
+	return stderrors.New(stackTrace)
+}
+
 // Like [BuildWithPlatform] but with a pre-initialized dockerui.Client
 func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
-	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *dalec.DockerImageSpec, *dalec.DockerImageSpec, error) {
+	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (_ gwclient.Reference, _ *dalec.DockerImageSpec, _ *dalec.DockerImageSpec, retErr error) {
+		defer func() {
+			if r := recover(); r != nil {
+				trace := getPanicStack()
+				recErr := fmt.Errorf("recovered from panic in build: %+v", r)
+				retErr = stderrors.Join(recErr, trace)
+			}
+		}()
+
 		spec, err := LoadSpec(ctx, dc, platform)
 		if err != nil {
 			return nil, nil, nil, err
@@ -141,8 +167,12 @@ func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client,
 // image in the build context matching the image ref.
 //
 // This follows the behavior of of the dockerfile frontend.
-func GetBaseImage(sOpt dalec.SourceOpts, ref string) llb.State {
+func GetBaseImage(sOpt dalec.SourceOpts, ref string, opts ...llb.ConstraintsOpt) llb.State {
 	return llb.Scratch().Async(func(ctx context.Context, _ llb.State, c *llb.Constraints) (llb.State, error) {
+		for _, o := range opts {
+			o.SetConstraintsOption(c)
+		}
+
 		fromClient, err := sOpt.GetContext(ref, dalec.WithConstraint(c))
 		if err != nil {
 			return llb.Scratch(), err
@@ -151,7 +181,8 @@ func GetBaseImage(sOpt dalec.SourceOpts, ref string) llb.State {
 		if fromClient != nil {
 			return *fromClient, nil
 		}
-		return llb.Image(ref, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraint(c)), nil
+
+		return llb.Image(ref, dalec.WithConstraint(c), llb.WithMetaResolver(sOpt.Resolver)), nil
 	})
 }
 
diff --git a/frontend/debug/handle_gomod.go b/frontend/debug/handle_gomod.go
index b9d58e319..8624579f9 100644
--- a/frontend/debug/handle_gomod.go
+++ b/frontend/debug/handle_gomod.go
@@ -16,7 +16,7 @@ const keyGomodWorker = "context:gomod-worker"
 // Gomods outputs all the gomodule dependencies for the spec
 func Gomods(ctx context.Context, client gwclient.Client) (*client.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -34,7 +34,7 @@ func Gomods(ctx context.Context, client gwclient.Client) (*client.Result, error)
 				Run(llb.Shlex("apk add --no-cache go git ca-certificates patch")).Root()
 		}
 
-		st, err := spec.GomodDeps(sOpt, worker)
+		st, err := spec.GomodDeps(sOpt, worker, dalec.Platform(platform))
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/frontend/debug/handle_sources.go b/frontend/debug/handle_sources.go
index eeb64357f..62efbfa9d 100644
--- a/frontend/debug/handle_sources.go
+++ b/frontend/debug/handle_sources.go
@@ -14,7 +14,7 @@ import (
 // Sources is a handler that outputs all the sources.
 func Sources(ctx context.Context, client gwclient.Client) (*client.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -53,7 +53,7 @@ func Sources(ctx context.Context, client gwclient.Client) (*client.Result, error
 func PatchedSources(ctx context.Context, client gwclient.Client) (*client.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
 		const keyPatchedSourcesWorker = "context:patched-sources-worker"
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -69,12 +69,13 @@ func PatchedSources(ctx context.Context, client gwclient.Client) (*client.Result
 				Run(llb.Shlex("apk add --no-cache go git ca-certificates patch")).Root()
 		}
 
-		sources, err := dalec.Sources(spec, sOpt)
+		pc := dalec.Platform(platform)
+		sources, err := dalec.Sources(spec, sOpt, pc)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		sources = dalec.PatchSources(worker, spec, sources)
+		sources = dalec.PatchSources(worker, spec, sources, pc)
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/frontend/gateway.go b/frontend/gateway.go
index 2072bfaad..d2ef53f4e 100644
--- a/frontend/gateway.go
+++ b/frontend/gateway.go
@@ -13,6 +13,7 @@ import (
 	"github.com/moby/buildkit/solver/pb"
 	"github.com/moby/buildkit/util/bklog"
 	"github.com/opencontainers/go-digest"
+	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 )
 
@@ -55,12 +56,12 @@ func getDockerfile(ctx context.Context, client gwclient.Client, build *dalec.Sou
 // ForwarderFromClient creates a [dalec.ForwarderFunc] from a gateway client.
 // This is used for forwarding builds to other frontends in [dalec.Source2LLBGetter]
 func ForwarderFromClient(ctx context.Context, client gwclient.Client) dalec.ForwarderFunc {
-	return func(st llb.State, spec *dalec.SourceBuild) (llb.State, error) {
+	return func(st llb.State, spec *dalec.SourceBuild, opts ...llb.ConstraintsOpt) (llb.State, error) {
 		if spec == nil {
 			spec = &dalec.SourceBuild{}
 		}
 
-		def, err := st.Marshal(ctx)
+		def, err := st.Marshal(ctx, opts...)
 		if err != nil {
 			return llb.Scratch(), err
 		}
@@ -101,10 +102,11 @@ func GetBuildArg(client gwclient.Client, k string) (string, bool) {
 	return "", false
 }
 
-func SourceOptFromUIClient(ctx context.Context, c gwclient.Client, dc *dockerui.Client) dalec.SourceOpts {
+func SourceOptFromUIClient(ctx context.Context, c gwclient.Client, dc *dockerui.Client, platform *ocispecs.Platform) dalec.SourceOpts {
 	return dalec.SourceOpts{
-		Resolver: c,
-		Forward:  ForwarderFromClient(ctx, c),
+		TargetPlatform: platform,
+		Resolver:       c,
+		Forward:        ForwarderFromClient(ctx, c),
 		GetContext: func(ref string, opts ...llb.LocalOption) (*llb.State, error) {
 			if ref == dockerui.DefaultLocalNameContext {
 				return dc.MainContext(ctx, opts...)
@@ -114,6 +116,7 @@ func SourceOptFromUIClient(ctx context.Context, c gwclient.Client, dc *dockerui.
 				AsyncLocalOpts: func() []llb.LocalOption {
 					return opts
 				},
+				Platform: platform,
 			})
 			if err != nil {
 				return nil, err
@@ -127,12 +130,12 @@ func SourceOptFromUIClient(ctx context.Context, c gwclient.Client, dc *dockerui.
 	}
 }
 
-func SourceOptFromClient(ctx context.Context, c gwclient.Client) (dalec.SourceOpts, error) {
+func SourceOptFromClient(ctx context.Context, c gwclient.Client, platform *ocispecs.Platform) (dalec.SourceOpts, error) {
 	dc, err := dockerui.NewClient(c)
 	if err != nil {
 		return dalec.SourceOpts{}, err
 	}
-	return SourceOptFromUIClient(ctx, c, dc), nil
+	return SourceOptFromUIClient(ctx, c, dc, platform), nil
 }
 
 var (
diff --git a/frontend/mux.go b/frontend/mux.go
index c00bdd1c1..67e80adf3 100644
--- a/frontend/mux.go
+++ b/frontend/mux.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	stderrors "errors"
 	"fmt"
 	"path"
 	"slices"
@@ -541,9 +542,14 @@ func (d *clientWithCustomOpts) CurrentFrontend() (*llb.State, error) {
 func (m *BuildMux) Handler(opts ...func(context.Context, gwclient.Client, *BuildMux) error) gwclient.BuildFunc {
 	return func(ctx context.Context, client gwclient.Client) (_ *gwclient.Result, retErr error) {
 		defer func() {
-			if r := recover(); r != nil {
-				retErr = errors.Errorf("recovered panic in handler: %+v", r)
-			}
+			defer func() {
+				if r := recover(); r != nil {
+					trace := getPanicStack()
+					recErr := fmt.Errorf("recovered from panic in handler: %+v", r)
+					retErr = stderrors.Join(recErr, trace)
+				}
+			}()
+
 		}()
 
 		if !SupportsDiffMerge(client) {
diff --git a/frontend/test_runner.go b/frontend/test_runner.go
index 07aec3c8c..c78e31ace 100644
--- a/frontend/test_runner.go
+++ b/frontend/test_runner.go
@@ -14,6 +14,7 @@ import (
 	"github.com/moby/buildkit/client/llb"
 	gwclient "github.com/moby/buildkit/frontend/gateway/client"
 	"github.com/moby/buildkit/identity"
+	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 )
 
@@ -31,7 +32,7 @@ const (
 )
 
 // Run tests runs the tests defined in the spec against the given target container.
-func RunTests(ctx context.Context, client gwclient.Client, spec *dalec.Spec, ref gwclient.Reference, withTestDeps llb.StateOption, target string) error {
+func RunTests(ctx context.Context, client gwclient.Client, spec *dalec.Spec, ref gwclient.Reference, withTestDeps llb.StateOption, target string, platform *ocispecs.Platform) error {
 	if skipVar := client.BuildOpts().Opts["build-arg:"+"DALEC_SKIP_TESTS"]; skipVar != "" {
 		skip, err := strconv.ParseBool(skipVar)
 		if err != nil {
@@ -67,7 +68,7 @@ func RunTests(ctx context.Context, client gwclient.Client, spec *dalec.Spec, ref
 		return err
 	}
 
-	sOpt, err := SourceOptFromClient(ctx, client)
+	sOpt, err := SourceOptFromClient(ctx, client, platform)
 	if err != nil {
 		return err
 	}
@@ -76,6 +77,7 @@ func RunTests(ctx context.Context, client gwclient.Client, spec *dalec.Spec, ref
 		st     llb.State
 		t      *dalec.TestSpec
 		stdios map[int]llb.State
+		opts   []llb.ConstraintsOpt
 	}
 
 	ctrWithDeps := ctr.With(withTestDeps)
@@ -93,7 +95,7 @@ func RunTests(ctx context.Context, client gwclient.Client, spec *dalec.Spec, ref
 		pg := llb.ProgressGroup(identity.NewID(), "Test: "+path.Join(target, test.Name), false)
 
 		for _, sm := range test.Mounts {
-			st, err := sm.Spec.AsMount(internalMountSourceName, sOpt, pg)
+			st, err := sm.Spec.AsMount(internalMountSourceName, sOpt, pg, dalec.Platform(platform))
 			if err != nil {
 				return err
 			}
@@ -119,18 +121,18 @@ func RunTests(ctx context.Context, client gwclient.Client, spec *dalec.Spec, ref
 				if step.Stdin != "" {
 					needsStdioMount = true
 					stepOpts = append(stepOpts, llb.AddEnv("STDIN_FILE", filepath.Join("/tmp", id, "stdin")))
-					ioSt = ioSt.File(llb.Mkfile("stdin", 0444, []byte(step.Stdin)))
+					ioSt = ioSt.File(llb.Mkfile("stdin", 0444, []byte(step.Stdin)), pg)
 				}
 				if !step.Stdout.IsEmpty() {
 					needsStdioMount = true
 					stepOpts = append(stepOpts, llb.AddEnv("STDOUT_FILE", path.Join("/tmp", id, "stdout")))
-					ioSt = ioSt.File(llb.Mkfile("stdout", 0664, nil))
+					ioSt = ioSt.File(llb.Mkfile("stdout", 0664, nil), pg)
 				}
 
 				if !step.Stderr.IsEmpty() {
 					needsStdioMount = true
 					stepOpts = append(stepOpts, llb.AddEnv("STDERR_FILE", path.Join("/tmp", id, "stderr")))
-					ioSt = ioSt.File(llb.Mkfile("stderr", 0664, nil))
+					ioSt = ioSt.File(llb.Mkfile("stderr", 0664, nil), pg)
 				}
 
 				cmd, err := shlex.Split(step.Command)
@@ -165,9 +167,9 @@ func RunTests(ctx context.Context, client gwclient.Client, spec *dalec.Spec, ref
 				worker = est.Root()
 			}
 
-			runs = append(runs, testPair{st: worker, t: test, stdios: ios})
+			runs = append(runs, testPair{st: worker, t: test, stdios: ios, opts: []llb.ConstraintsOpt{pg, dalec.Platform(platform)}})
 		} else {
-			runs = append(runs, testPair{st: base, t: test})
+			runs = append(runs, testPair{st: base, t: test, opts: []llb.ConstraintsOpt{pg, dalec.Platform(platform)}})
 		}
 	}
 
@@ -177,7 +179,7 @@ func RunTests(ctx context.Context, client gwclient.Client, spec *dalec.Spec, ref
 		pair := pair
 		wg.Add(1)
 		go func() {
-			if err := runTest(ctx, pair.t, pair.st, pair.stdios, client); err != nil {
+			if err := runTest(ctx, pair.t, pair.st, pair.stdios, client, pair.opts...); err != nil {
 				errs.Append(errors.Wrap(err, "FAILED: "+path.Join(target, pair.t.Name)))
 			}
 			wg.Done()
@@ -193,8 +195,8 @@ type frontendClient interface {
 	CurrentFrontend() (*llb.State, error)
 }
 
-func runTest(ctx context.Context, t *dalec.TestSpec, st llb.State, ios map[int]llb.State, client gwclient.Client) error {
-	def, err := st.Marshal(ctx)
+func runTest(ctx context.Context, t *dalec.TestSpec, st llb.State, ios map[int]llb.State, client gwclient.Client, opts ...llb.ConstraintsOpt) error {
+	def, err := st.Marshal(ctx, opts...)
 	if err != nil {
 		return err
 	}
@@ -245,7 +247,7 @@ func runTest(ctx context.Context, t *dalec.TestSpec, st llb.State, ios map[int]l
 	}
 
 	for i, st := range ios {
-		def, err := st.Marshal(ctx)
+		def, err := st.Marshal(ctx, opts...)
 		if err != nil {
 			outErr = stderrors.Join(errors.Wrap(err, "failed to marshal stdio state"))
 			continue
diff --git a/helpers.go b/helpers.go
index 58f0d30f2..f39faae2a 100644
--- a/helpers.go
+++ b/helpers.go
@@ -634,3 +634,12 @@ func BaseImageConfig(platform *ocispecs.Platform) *DockerImageSpec {
 
 	return img
 }
+
+// Platform returns a [llb.ConstraintsOpt] that sets the platform to the provided platform
+// If the platform is nil, the [llb.ConstraintOpt] is a no-op.
+func Platform(platform *ocispecs.Platform) llb.ConstraintsOpt {
+	if platform == nil {
+		return constraintsOptFunc(func(c *llb.Constraints) {})
+	}
+	return llb.Platform(*platform)
+}
diff --git a/source.go b/source.go
index 8aeb46c16..5f8a845d8 100644
--- a/source.go
+++ b/source.go
@@ -15,6 +15,7 @@ import (
 	"github.com/moby/buildkit/frontend/dockerui"
 	"github.com/moby/buildkit/identity"
 	"github.com/moby/buildkit/util/gitutil"
+	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 )
 
@@ -225,12 +226,13 @@ var (
 
 type LLBGetter func(sOpts SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error)
 
-type ForwarderFunc func(llb.State, *SourceBuild) (llb.State, error)
+type ForwarderFunc func(llb.State, *SourceBuild, ...llb.ConstraintsOpt) (llb.State, error)
 
 type SourceOpts struct {
-	Resolver   llb.ImageMetaResolver
-	Forward    ForwarderFunc
-	GetContext func(string, ...llb.LocalOption) (*llb.State, error)
+	Resolver       llb.ImageMetaResolver
+	Forward        ForwarderFunc
+	GetContext     func(string, ...llb.LocalOption) (*llb.State, error)
+	TargetPlatform *ocispecs.Platform
 }
 
 func (s *Source) asState(name string, forMount bool, sOpt SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) {
diff --git a/source_build.go b/source_build.go
index 3d044142e..f85f8650f 100644
--- a/source_build.go
+++ b/source_build.go
@@ -61,7 +61,7 @@ func (src *SourceBuild) AsState(name string, sOpt SourceOpts, opts ...llb.Constr
 		st = llb.Scratch()
 	}
 
-	st, err = sOpt.Forward(st, src)
+	st, err = sOpt.Forward(st, src, opts...)
 	if err != nil {
 		return llb.Scratch(), err
 	}
diff --git a/source_test.go b/source_test.go
index 103a36b4b..c187c66fa 100644
--- a/source_test.go
+++ b/source_test.go
@@ -837,7 +837,7 @@ func getSourceOp(ctx context.Context, t *testing.T, src Source) []*pb.Op {
 		if src.Build.Source.Inline == nil || src.Build.Source.Inline.File == nil {
 			t.Fatal("Cannot test from a Dockerfile without inline content")
 		}
-		sOpt.Forward = func(_ llb.State, build *SourceBuild) (llb.State, error) {
+		sOpt.Forward = func(_ llb.State, build *SourceBuild, _ ...llb.ConstraintsOpt) (llb.State, error) {
 			// Note, we can't really test anything other than inline here because we don't have access to the actual buildkit client,
 			// so we can't extract extract the dockerfile from the input state (nor do we have any input state)
 			src := []byte(src.Build.Source.Inline.File.Contents)
diff --git a/targets/linux/deb/distro/pkg.go b/targets/linux/deb/distro/pkg.go
index 54ef7f09d..908154b2d 100644
--- a/targets/linux/deb/distro/pkg.go
+++ b/targets/linux/deb/distro/pkg.go
@@ -170,20 +170,21 @@ func (cfg *Config) RunTests(ctx context.Context, client gwclient.Client, _ llb.S
 	}
 
 	withTestDeps := cfg.InstallTestDeps(sOpt, targetKey, spec, opts...)
-	err = frontend.RunTests(ctx, client, spec, ref, withTestDeps, targetKey)
+	err = frontend.RunTests(ctx, client, spec, ref, withTestDeps, targetKey, sOpt.TargetPlatform)
 	return ref, err
 }
 
 func (cfg *Config) HandleSourcePkg(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
 
 		pg := dalec.ProgressGroup(spec.Name)
+		pc := dalec.Platform(platform)
 
-		worker, err := cfg.Worker(sOpt, pg)
+		worker, err := cfg.Worker(sOpt, pg, pc)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -201,7 +202,7 @@ func (cfg *Config) HandleSourcePkg(ctx context.Context, client gwclient.Client)
 			return nil, nil, err
 		}
 
-		st, err := deb.SourcePackage(ctx, sOpt, worker.With(extraPaths), spec, targetKey, versionID, cfg, pg)
+		st, err := deb.SourcePackage(ctx, sOpt, worker.With(extraPaths), spec, targetKey, versionID, cfg, pg, pc)
 		if err != nil {
 			return nil, nil, errors.Wrap(err, "error building source package")
 		}
diff --git a/targets/linux/deb/distro/worker.go b/targets/linux/deb/distro/worker.go
index b532e4cf7..3dabb8662 100644
--- a/targets/linux/deb/distro/worker.go
+++ b/targets/linux/deb/distro/worker.go
@@ -15,7 +15,7 @@ import (
 
 func (cfg *Config) HandleWorker(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -24,14 +24,14 @@ func (cfg *Config) HandleWorker(ctx context.Context, client gwclient.Client) (*g
 		if platform != nil {
 			p = *platform
 		}
-		pOpt := llb.Platform(p)
+		pc := llb.Platform(p)
 
-		st, err := cfg.Worker(sOpt, pOpt)
+		st, err := cfg.Worker(sOpt, pc)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		def, err := st.Marshal(ctx)
+		def, err := st.Marshal(ctx, pc)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -75,7 +75,7 @@ func (cfg *Config) Worker(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (ll
 		}
 	}
 
-	base := frontend.GetBaseImage(sOpt, cfg.ImageRef).
+	base := frontend.GetBaseImage(sOpt, cfg.ImageRef, opts...).
 		Run(
 			dalec.WithConstraints(opts...),
 			AptInstall(cfg.BuilderPackages, opts...),
@@ -89,7 +89,7 @@ func (cfg *Config) Worker(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (ll
 		// environment. This is only needed because certain tests (which
 		// are using this customized builder image) are checking for files
 		// that are being excluded by this config file.
-		File(llb.Rm("/etc/dpkg/dpkg.cfg.d/excludes", llb.WithAllowNotFound(true)))
+		File(llb.Rm("/etc/dpkg/dpkg.cfg.d/excludes", llb.WithAllowNotFound(true)), opts...)
 
 	return base, nil
 }
diff --git a/targets/linux/distro_handler.go b/targets/linux/distro_handler.go
index ebd4e7e58..e980b6626 100644
--- a/targets/linux/distro_handler.go
+++ b/targets/linux/distro_handler.go
@@ -85,18 +85,20 @@ func resolveConfig(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec,
 func HandleContainer(c DistroConfig) gwclient.BuildFunc {
 	return func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
 		return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-			sOpt, err := frontend.SourceOptFromClient(ctx, client)
+			sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 			if err != nil {
 				return nil, nil, err
 			}
 
 			pg := dalec.ProgressGroup(spec.Name)
-			worker, err := c.Worker(sOpt, pg)
+			pc := dalec.Platform(platform)
+
+			worker, err := c.Worker(sOpt, pg, pc)
 			if err != nil {
 				return nil, nil, err
 			}
 
-			deb, err := c.BuildPkg(ctx, client, worker, sOpt, spec, targetKey, pg)
+			deb, err := c.BuildPkg(ctx, client, worker, sOpt, spec, targetKey, pg, pc)
 			if err != nil {
 				return nil, nil, err
 			}
@@ -106,12 +108,12 @@ func HandleContainer(c DistroConfig) gwclient.BuildFunc {
 				return nil, nil, err
 			}
 
-			ctr, err := c.BuildContainer(ctx, client, worker, sOpt, spec, targetKey, deb)
+			ctr, err := c.BuildContainer(ctx, client, worker, sOpt, spec, targetKey, deb, pg, pc)
 			if err != nil {
 				return nil, nil, err
 			}
 
-			ref, err := c.RunTests(ctx, client, worker, spec, sOpt, ctr, targetKey, pg)
+			ref, err := c.RunTests(ctx, client, worker, spec, sOpt, ctr, targetKey, pg, pc)
 			return ref, img, err
 		})
 	}
@@ -125,22 +127,23 @@ func HandlePackage(cfg DistroConfig) gwclient.BuildFunc {
 			}
 
 			pg := dalec.ProgressGroup("Building " + targetKey + " package: " + spec.Name)
-			sOpt, err := frontend.SourceOptFromClient(ctx, client)
+			sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 			if err != nil {
 				return nil, nil, err
 			}
 
-			worker, err := cfg.Worker(sOpt, pg)
+			pc := dalec.Platform(platform)
+			worker, err := cfg.Worker(sOpt, pg, pc)
 			if err != nil {
 				return nil, nil, errors.Wrap(err, "error building worker container")
 			}
 
-			pkgSt, err := cfg.BuildPkg(ctx, client, worker, sOpt, spec, targetKey, pg)
+			pkgSt, err := cfg.BuildPkg(ctx, client, worker, sOpt, spec, targetKey, pg, pc)
 			if err != nil {
 				return nil, nil, err
 			}
 
-			def, err := pkgSt.Marshal(ctx, pg)
+			def, err := pkgSt.Marshal(ctx, pc)
 			if err != nil {
 				return nil, nil, fmt.Errorf("error marshalling llb: %w", err)
 			}
@@ -161,12 +164,12 @@ func HandlePackage(cfg DistroConfig) gwclient.BuildFunc {
 				return ref, nil, err
 			}
 
-			ctr, err := cfg.BuildContainer(ctx, client, worker, sOpt, spec, targetKey, pkgSt)
+			ctr, err := cfg.BuildContainer(ctx, client, worker, sOpt, spec, targetKey, pkgSt, pg, pc)
 			if err != nil {
 				return ref, nil, err
 			}
 
-			if ref, err := cfg.RunTests(ctx, client, worker, spec, sOpt, ctr, targetKey, pg); err != nil {
+			if ref, err := cfg.RunTests(ctx, client, worker, spec, sOpt, ctr, targetKey, pg, pc); err != nil {
 				cfg, _ := BuildImageConfig(ctx, sOpt, spec, platform, targetKey)
 				return ref, cfg, err
 			}
diff --git a/targets/linux/rpm/distro/container.go b/targets/linux/rpm/distro/container.go
index 91c1937cf..91c98513f 100644
--- a/targets/linux/rpm/distro/container.go
+++ b/targets/linux/rpm/distro/container.go
@@ -91,12 +91,13 @@ func (cfg *Config) HandleDepsOnly(ctx context.Context, client gwclient.Client) (
 
 		pg := dalec.ProgressGroup("Build " + targetKey + " deps-only container for: " + spec.Name)
 
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		worker, err := cfg.Worker(sOpt, pg)
+		pc := dalec.Platform(platform)
+		worker, err := cfg.Worker(sOpt, pg, pc)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -109,12 +110,12 @@ func (cfg *Config) HandleDepsOnly(ctx context.Context, client gwclient.Client) (
 					DnfDownloadAllDeps("/tmp/rpms/RPMS/$(uname -m)"))).Root()
 			rpmDir = llb.Scratch().File(llb.Copy(withDownloads, "/tmp/rpms", "/", dalec.WithDirContentsOnly()))
 		}
-		ctr, err := cfg.BuildContainer(ctx, client, worker, sOpt, spec, targetKey, rpmDir, pg)
+		ctr, err := cfg.BuildContainer(ctx, client, worker, sOpt, spec, targetKey, rpmDir, pg, pc)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		def, err := ctr.Marshal(ctx, pg)
+		def, err := ctr.Marshal(ctx, pc)
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/targets/linux/rpm/distro/debug.go b/targets/linux/rpm/distro/debug.go
index 3cf52c906..23144eb05 100644
--- a/targets/linux/rpm/distro/debug.go
+++ b/targets/linux/rpm/distro/debug.go
@@ -21,8 +21,8 @@ import (
 // It is most useful for `HandleSources` handler in which we aren't building a full worker image with
 // build dependencies because we aren't executing build steps, but we may still have source generators
 // which depend on `build` dependencies in the spec in order to run.
-func (c *Config) DebugWorker(ctx context.Context, client gwclient.Client, spec *dalec.Spec, targetKey string, sOpt dalec.SourceOpts) (llb.State, error) {
-	worker, err := c.Worker(sOpt)
+func (c *Config) DebugWorker(ctx context.Context, client gwclient.Client, spec *dalec.Spec, targetKey string, sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) {
+	worker, err := c.Worker(sOpt, opts...)
 	if err != nil {
 		return llb.Scratch(), err
 	}
@@ -38,7 +38,7 @@ func (c *Config) DebugWorker(ctx context.Context, client gwclient.Client, spec *
 			return llb.Scratch(), errors.New("spec contains go modules but does not have golang in build deps")
 		}
 
-		worker = worker.With(c.InstallBuildDeps(ctx, client, spec, targetKey))
+		worker = worker.With(c.InstallBuildDeps(ctx, client, spec, sOpt, targetKey))
 	}
 
 	return worker, nil
@@ -51,24 +51,25 @@ func (c *Config) HandleBuildroot(ctx context.Context, client gwclient.Client) (*
 		}
 
 		pg := dalec.ProgressGroup("Setting up " + targetKey + " rpm buildroot: " + spec.Name)
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		worker, err := c.Worker(sOpt, pg)
+		pc := dalec.Platform(platform)
+		worker, err := c.Worker(sOpt, pg, pc)
 		if err != nil {
 			return nil, nil, errors.Wrap(err, "error building worker container")
 		}
 
-		worker = worker.With(c.InstallBuildDeps(ctx, client, spec, targetKey, pg))
+		worker = worker.With(c.InstallBuildDeps(ctx, client, spec, sOpt, targetKey, pg, pc))
 
 		br, err := rpm.SpecToBuildrootLLB(worker, spec, sOpt, targetKey, pg)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		def, err := br.Marshal(ctx)
+		def, err := br.Marshal(ctx, pc)
 		if err != nil {
 			return nil, nil, fmt.Errorf("error marshalling llb: %w", err)
 		}
@@ -96,17 +97,18 @@ func (c *Config) HandleBuildroot(ctx context.Context, client gwclient.Client) (*
 
 func (c *Config) HandleSources(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		worker, err := c.DebugWorker(ctx, client, spec, targetKey, sOpt)
+		pc := dalec.Platform(platform)
+		worker, err := c.DebugWorker(ctx, client, spec, targetKey, sOpt, pc)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		sources, err := rpm.ToSourcesLLB(worker, spec, sOpt)
+		sources, err := rpm.ToSourcesLLB(worker, spec, sOpt, pc)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -114,7 +116,7 @@ func (c *Config) HandleSources(ctx context.Context, client gwclient.Client) (*gw
 		// Now we can merge sources into the desired path
 		st := dalec.MergeAtPath(llb.Scratch(), sources, "/SOURCES")
 
-		def, err := st.Marshal(ctx)
+		def, err := st.Marshal(ctx, pc)
 		if err != nil {
 			return nil, nil, fmt.Errorf("error marshalling llb: %w", err)
 		}
@@ -136,12 +138,13 @@ func (c *Config) HandleSources(ctx context.Context, client gwclient.Client) (*gw
 
 func (c *Config) HandleSpec(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		st, err := rpm.ToSpecLLB(spec, llb.Scratch(), targetKey, "")
+		pc := dalec.Platform(platform)
+		st, err := rpm.ToSpecLLB(spec, llb.Scratch(), targetKey, "", pc)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		def, err := st.Marshal(ctx)
+		def, err := st.Marshal(ctx, pc)
 		if err != nil {
 			return nil, nil, fmt.Errorf("error marshalling llb: %w", err)
 		}
diff --git a/targets/linux/rpm/distro/dnf_install.go b/targets/linux/rpm/distro/dnf_install.go
index be6e2b217..3ec9a4b49 100644
--- a/targets/linux/rpm/distro/dnf_install.go
+++ b/targets/linux/rpm/distro/dnf_install.go
@@ -228,7 +228,7 @@ func (cfg *Config) installBuildDepsPackage(worker llb.State, target string, pack
 	}
 }
 
-func (cfg *Config) InstallBuildDeps(ctx context.Context, client gwclient.Client, spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) llb.StateOption {
+func (cfg *Config) InstallBuildDeps(ctx context.Context, client gwclient.Client, spec *dalec.Spec, sOpt dalec.SourceOpts, targetKey string, opts ...llb.ConstraintsOpt) llb.StateOption {
 	deps := spec.GetBuildDeps(targetKey)
 	if len(deps) == 0 {
 		return func(in llb.State) llb.State { return in }
@@ -236,7 +236,7 @@ func (cfg *Config) InstallBuildDeps(ctx context.Context, client gwclient.Client,
 
 	repos := spec.GetBuildRepos(targetKey)
 
-	sOpt, err := frontend.SourceOptFromClient(ctx, client)
+	sOpt, err := frontend.SourceOptFromClient(ctx, client, sOpt.TargetPlatform)
 	if err != nil {
 		return nil
 	}
diff --git a/targets/linux/rpm/distro/pkg.go b/targets/linux/rpm/distro/pkg.go
index 986caa42d..b31d1123f 100644
--- a/targets/linux/rpm/distro/pkg.go
+++ b/targets/linux/rpm/distro/pkg.go
@@ -25,7 +25,7 @@ func (c *Config) Validate(spec *dalec.Spec) error {
 }
 
 func (c *Config) BuildPkg(ctx context.Context, client gwclient.Client, worker llb.State, sOpt dalec.SourceOpts, spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) (llb.State, error) {
-	worker = worker.With(c.InstallBuildDeps(ctx, client, spec, targetKey, opts...))
+	worker = worker.With(c.InstallBuildDeps(ctx, client, spec, sOpt, targetKey, opts...))
 
 	br, err := rpm.SpecToBuildrootLLB(worker, spec, sOpt, targetKey, opts...)
 	if err != nil {
@@ -61,7 +61,7 @@ func (cfg *Config) RunTests(ctx context.Context, client gwclient.Client, worker
 	}
 
 	withTestDeps := cfg.InstallTestDeps(worker, sOpt, targetKey, spec, opts...)
-	err = frontend.RunTests(ctx, client, spec, ref, withTestDeps, targetKey)
+	err = frontend.RunTests(ctx, client, spec, ref, withTestDeps, targetKey, sOpt.TargetPlatform)
 	return ref, errors.Wrap(err, "TESTS FAILED")
 }
 
diff --git a/targets/linux/rpm/distro/worker.go b/targets/linux/rpm/distro/worker.go
index e899c2425..145a56e4a 100644
--- a/targets/linux/rpm/distro/worker.go
+++ b/targets/linux/rpm/distro/worker.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/Azure/dalec"
 	"github.com/Azure/dalec/frontend"
-	"github.com/containerd/platforms"
 	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/client/llb/sourceresolver"
 	gwclient "github.com/moby/buildkit/frontend/gateway/client"
@@ -17,23 +16,18 @@ import (
 // TODO: can this be refactored to share code?
 func (cfg *Config) HandleWorker(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		p := platforms.DefaultSpec()
-		if platform != nil {
-			p = *platform
-		}
-		pOpt := llb.Platform(p)
-
-		st, err := cfg.Worker(sOpt, pOpt)
+		pc := dalec.Platform(platform)
+		st, err := cfg.Worker(sOpt, pc)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		def, err := st.Marshal(ctx)
+		def, err := st.Marshal(ctx, pc)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -77,7 +71,7 @@ func (cfg *Config) Worker(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (ll
 		}
 	}
 
-	base := frontend.GetBaseImage(sOpt, cfg.ImageRef).
+	base := frontend.GetBaseImage(sOpt, cfg.ImageRef, opts...).
 		Run(
 			dalec.WithConstraints(opts...),
 			cfg.Install(cfg.BuilderPackages),
diff --git a/targets/windows/handle_container.go b/targets/windows/handle_container.go
index 330ce42e3..2572ec2fd 100644
--- a/targets/windows/handle_container.go
+++ b/targets/windows/handle_container.go
@@ -47,7 +47,7 @@ func handleContainer(ctx context.Context, client gwclient.Client) (*gwclient.Res
 		return nil, fmt.Errorf("multi-platform output is not supported")
 	}
 
-	sOpt := frontend.SourceOptFromUIClient(ctx, client, dc)
+	sOpt := frontend.SourceOptFromUIClient(ctx, client, dc, nil)
 
 	spec, err := frontend.LoadSpec(ctx, dc, nil)
 	if err != nil {
diff --git a/targets/windows/handle_zip.go b/targets/windows/handle_zip.go
index 1c0046207..9bb319fb4 100644
--- a/targets/windows/handle_zip.go
+++ b/targets/windows/handle_zip.go
@@ -23,7 +23,7 @@ const (
 
 func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, nil)
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/targets/windows/handler.go b/targets/windows/handler.go
index d0bef039e..cfefff784 100644
--- a/targets/windows/handler.go
+++ b/targets/windows/handler.go
@@ -8,7 +8,6 @@ import (
 	"github.com/Azure/dalec/frontend"
 	"github.com/Azure/dalec/targets/linux/deb/distro"
 	"github.com/containerd/platforms"
-	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/client/llb/sourceresolver"
 	gwclient "github.com/moby/buildkit/frontend/gateway/client"
 	bktargets "github.com/moby/buildkit/frontend/subrequests/targets"
@@ -72,17 +71,12 @@ func Handle(ctx context.Context, client gwclient.Client) (*gwclient.Result, erro
 
 func handleWorker(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
 	return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, nil)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		var opts []llb.ConstraintsOpt
-		if platform != nil {
-			opts = append(opts, llb.Platform(*platform))
-		}
-
-		st, err := distroConfig.Worker(sOpt, opts...)
+		st, err := distroConfig.Worker(sOpt)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -100,9 +94,7 @@ func handleWorker(ctx context.Context, client gwclient.Client) (*gwclient.Result
 			return nil, nil, err
 		}
 
-		_, _, dt, err := client.ResolveImageConfig(ctx, workerImgRef, sourceresolver.Opt{
-			Platform: platform,
-		})
+		_, _, dt, err := client.ResolveImageConfig(ctx, workerImgRef, sourceresolver.Opt{})
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/test/linux_target_test.go b/test/linux_target_test.go
index 5b908549d..4e388335c 100644
--- a/test/linux_target_test.go
+++ b/test/linux_target_test.go
@@ -13,6 +13,7 @@ import (
 	"time"
 
 	"github.com/Azure/dalec"
+	"github.com/containerd/platforms"
 	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/exporter/containerimage/exptypes"
 	gwclient "github.com/moby/buildkit/frontend/gateway/client"
@@ -83,6 +84,9 @@ type testLinuxConfig struct {
 	Release OSRelease
 
 	SkipStripTest bool
+
+	Platforms         []ocispecs.Platform
+	PackageOutputPath func(spec *dalec.Spec, platform ocispecs.Platform) string
 }
 
 type OSRelease struct {
@@ -98,6 +102,33 @@ func (cfg *testLinuxConfig) GetPackage(name string) string {
 	return name
 }
 
+func rpmTargetOutputPath(id string) func(spec *dalec.Spec, platform ocispecs.Platform) string {
+	return func(spec *dalec.Spec, platform ocispecs.Platform) string {
+		var arch string
+		switch platform.Architecture {
+		case "amd64":
+			arch = "x86_64"
+		case "arm64":
+			arch = "aarch64"
+		case "arm":
+			// this is not perfect, but all we really support in our tests for now, so its fine.
+			arch = "armv7l"
+		}
+		return fmt.Sprintf("/RPMS/%s/%s-%s-%s.%s.%s.rpm", arch, spec.Name, spec.Version, spec.Revision, id, arch)
+	}
+}
+
+func debTargetOutputPath(id string) func(spec *dalec.Spec, platform ocispecs.Platform) string {
+	return func(spec *dalec.Spec, platform ocispecs.Platform) string {
+		arch := platform.Architecture
+		if arch == "arm" {
+			// this is not perfect, but all we really support in our tests for now, so its fine.
+			arch = "armhf"
+		}
+		return fmt.Sprintf("%s_%s-%s_%s.deb", spec.Name, spec.Version, id+"u"+spec.Revision, arch)
+	}
+}
+
 func testLinuxDistro(ctx context.Context, t *testing.T, testConfig testLinuxConfig) {
 	t.Run("Fail when non-zero exit code during build", func(t *testing.T) {
 		t.Parallel()
@@ -1575,6 +1606,11 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot
 		t.Parallel()
 		testDisableStrip(ctx, t, testConfig)
 	})
+
+	t.Run("cross platform", func(t *testing.T) {
+		t.Parallel()
+		testTargetPlatform(ctx, t, testConfig)
+	})
 }
 
 func testCustomLinuxWorker(ctx context.Context, t *testing.T, targetCfg targetConfig, workerCfg workerConfig) {
@@ -2447,3 +2483,67 @@ func testDisableStrip(ctx context.Context, t *testing.T, cfg testLinuxConfig) {
 		})
 	})
 }
+
+func testTargetPlatform(ctx context.Context, t *testing.T, cfg testLinuxConfig) {
+	ctx = startTestSpan(ctx, t)
+
+	testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) {
+		bp := readDefaultPlatform(ctx, t, client)
+
+		var tp ocispecs.Platform
+		for _, p := range cfg.Platforms {
+			if platforms.OnlyStrict(bp).Match(p) {
+				continue
+			}
+
+			tp = p
+
+			if bp.Architecture != "arm64" {
+				break
+			}
+			// On arm64, let's try and build for armv7 to help speed up tests
+			// Also makes sure that building arm on arm64 gives the correct result since this can run natively (usually)
+			if tp.Architecture == "arm" {
+				break
+			}
+		}
+
+		skip.If(t, tp.OS == "", "No other platforms available to test")
+		assert.Assert(t, tp.Architecture != bp.Architecture, "Target and build arches are the same")
+
+		spec := newSimpleSpec()
+		spec.Args = map[string]string{
+			"TARGETARCH": "",
+		}
+
+		spec.Build.Env = map[string]string{
+			"TARGETARCH": "$TARGETARCH",
+		}
+
+		spec.Build.Steps = []dalec.BuildStep{
+			{
+				Command: fmt.Sprintf(`[ "${TARGETARCH}" = "%s" ]`, tp.Architecture),
+			},
+		}
+
+		sr := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(cfg.Target.Package), withPlatform(tp))
+		res := solveT(ctx, t, client, sr)
+		ref, err := res.SingleRef()
+		assert.NilError(t, err)
+
+		_, err = ref.StatFile(ctx, gwclient.StatRequest{
+			Path: cfg.PackageOutputPath(spec, tp),
+		})
+		assert.NilError(t, err)
+
+		sr = newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(cfg.Target.Container), withPlatform(tp))
+		res = solveT(ctx, t, client, sr)
+		dt, ok := res.Metadata[exptypes.ExporterImageConfigKey]
+		assert.Assert(t, ok, "missing image config in result metadata")
+
+		var img dalec.DockerImageSpec
+		err = json.Unmarshal(dt, &img)
+		assert.NilError(t, err)
+		assert.Assert(t, platforms.OnlyStrict(tp).Match(img.Platform), "platform mismatch, expected %v, got %v", tp, img.Platform)
+	})
+}
diff --git a/test/target_almalinux_test.go b/test/target_almalinux_test.go
index dc828b889..6c9fc9242 100644
--- a/test/target_almalinux_test.go
+++ b/test/target_almalinux_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/Azure/dalec/targets/linux/rpm/almalinux"
+	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
 func TestAlmalinux9(t *testing.T) {
@@ -40,6 +41,11 @@ func TestAlmalinux9(t *testing.T) {
 			ID:        "almalinux",
 			VersionID: "9",
 		},
+		Platforms: []ocispecs.Platform{
+			{OS: "linux", Architecture: "amd64"},
+			{OS: "linux", Architecture: "arm64"},
+		},
+		PackageOutputPath: rpmTargetOutputPath("el9"),
 	})
 }
 
@@ -78,5 +84,10 @@ func TestAlmalinux8(t *testing.T) {
 			VersionID: "8",
 		},
 		SkipStripTest: true,
+		Platforms: []ocispecs.Platform{
+			{OS: "linux", Architecture: "amd64"},
+			{OS: "linux", Architecture: "arm64"},
+		},
+		PackageOutputPath: rpmTargetOutputPath("el8"),
 	})
 }
diff --git a/test/target_azlinux_test.go b/test/target_azlinux_test.go
index 38e39912a..070e1db5c 100644
--- a/test/target_azlinux_test.go
+++ b/test/target_azlinux_test.go
@@ -73,6 +73,11 @@ func TestMariner2(t *testing.T) {
 			ID:        "mariner",
 			VersionID: "2.0",
 		},
+		Platforms: []ocispecs.Platform{
+			{OS: "linux", Architecture: "amd64"},
+			{OS: "linux", Architecture: "arm64"},
+		},
+		PackageOutputPath: rpmTargetOutputPath("cm2"),
 	}
 
 	testLinuxDistro(ctx, t, cfg)
@@ -109,6 +114,11 @@ func TestAzlinux3(t *testing.T) {
 			ID:        "azurelinux",
 			VersionID: "3.0",
 		},
+		Platforms: []ocispecs.Platform{
+			{OS: "linux", Architecture: "amd64"},
+			{OS: "linux", Architecture: "arm64"},
+		},
+		PackageOutputPath: rpmTargetOutputPath("azl3"),
 	}
 	testLinuxDistro(ctx, t, cfg)
 	testAzlinuxExtra(ctx, t, cfg)
diff --git a/test/target_rockylinux_test.go b/test/target_rockylinux_test.go
index 29bbb702c..951a7e2e6 100644
--- a/test/target_rockylinux_test.go
+++ b/test/target_rockylinux_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/Azure/dalec/targets/linux/rpm/rockylinux"
+	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
 func TestRockylinux9(t *testing.T) {
@@ -40,6 +41,11 @@ func TestRockylinux9(t *testing.T) {
 			ID:        "rocky",
 			VersionID: "9",
 		},
+		Platforms: []ocispecs.Platform{
+			{OS: "linux", Architecture: "amd64"},
+			{OS: "linux", Architecture: "arm64"},
+		},
+		PackageOutputPath: rpmTargetOutputPath("el9"),
 	})
 }
 
@@ -78,5 +84,10 @@ func TestRockylinux8(t *testing.T) {
 			VersionID: "8",
 		},
 		SkipStripTest: true,
+		Platforms: []ocispecs.Platform{
+			{OS: "linux", Architecture: "amd64"},
+			{OS: "linux", Architecture: "arm64"},
+		},
+		PackageOutputPath: rpmTargetOutputPath("el8"),
 	})
 }
diff --git a/test/target_ubuntu_test.go b/test/target_ubuntu_test.go
index 2ff02dd47..9157fb772 100644
--- a/test/target_ubuntu_test.go
+++ b/test/target_ubuntu_test.go
@@ -58,6 +58,13 @@ func debLinuxTestConfigFor(targetKey string, cfg *distro.Config, opts ...func(*t
 			TestRepoConfig: ubuntuTestRepoConfig,
 			Constraints:    debConstraintsSymbols,
 		},
+
+		Platforms: []ocispecs.Platform{
+			{OS: "linux", Architecture: "amd64"},
+			{OS: "linux", Architecture: "arm64"},
+			{OS: "linux", Architecture: "arm", Variant: "v7"},
+		},
+		PackageOutputPath: debTargetOutputPath(cfg.VersionID),
 	}
 
 	for _, o := range opts {


Reviews:
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T22:31:43Z
user: login: DannyBrito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T14:39:12Z
user: login: DannyBrito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T14:40:30Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T14:50:38Z
user: login: DannyBrito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T14:58:35Z
user: login: DannyBrito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T14:59:02Z
user: login: DannyBrito
body: This PR lgtm. That said, I’m not sure I’d catch it if any changes are missing.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T15:13:38Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T15:51:18Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T16:12:04Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T16:12:15Z
user: login: DannyBrito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T16:23:21Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T16:25:27Z
user: login: pmengelbert
body: LGTM, thanks for this.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T14:39:14Z
user: login: pmengelbert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T14:52:23Z
user: login: pmengelbert
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T14:53:51Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T15:04:46Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T15:05:15Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T15:17:55Z
user: login: pmengelbert
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:19:59Z
user: login: adamperlin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T17:21:39Z
user: login: adamperlin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T17:21:50Z
user: login: adamperlin
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T17:22:39Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T18:18:45Z
user: login: cpuguy83
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T18:39:36Z

Review comments:
diff_hunk: @@ -142,6 +142,8 @@ jobs:
 
       - name: download deps
         run: go mod download
+      - name: Setup QEMU
+        run: docker run --rm --privileged upstream.azurecr.io/binfmt:latest --install all
path: .github/workflows/ci.yml
user: login: cpuguy83
body: TODO: Change this image ref
created_at: 2025-03-28T22:31:42Z
author_association: COLLABORATOR
diff_hunk: @@ -142,6 +142,8 @@ jobs:
 
       - name: download deps
         run: go mod download
+      - name: Setup QEMU
+        run: docker run --rm --privileged upstream.azurecr.io/binfmt:latest --install all
path: .github/workflows/ci.yml
user: login: DannyBrito
body: this registry has a purge task on that will get rid of images older than 30 or 90 days old. So, this might break at some point.
created_at: 2025-04-01T14:39:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,6 +142,8 @@ jobs:
 
       - name: download deps
         run: go mod download
+      - name: Setup QEMU
+        run: docker run --rm --privileged upstream.azurecr.io/binfmt:latest --install all
path: .github/workflows/ci.yml
user: login: DannyBrito
body: this task has an exclusion option so maybe we can add it there
created_at: 2025-04-01T14:40:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,6 +142,8 @@ jobs:
 
       - name: download deps
         run: go mod download
+      - name: Setup QEMU
+        run: docker run --rm --privileged upstream.azurecr.io/binfmt:latest --install all
path: .github/workflows/ci.yml
user: login: cpuguy83
body: I plan to change this ref.
created_at: 2025-04-01T14:50:38Z
author_association: COLLABORATOR
diff_hunk: @@ -51,24 +51,25 @@ func (c *Config) HandleBuildroot(ctx context.Context, client gwclient.Client) (*
 		}
 
 		pg := dalec.ProgressGroup("Setting up " + targetKey + " rpm buildroot: " + spec.Name)
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		worker, err := c.Worker(sOpt, pg)
+		pOpt := dalec.Platform(platform)
+		worker, err := c.Worker(sOpt, pg, pOpt)
path: targets/linux/rpm/distro/debug.go
user: login: DannyBrito
body: could you please consider changing the var `pOpt` in here. Having both `pOpt` and `sOpt` feels a bit hard to keep track of both at least for me
created_at: 2025-04-01T14:58:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,7 +155,8 @@ func GetBaseImage(sOpt dalec.SourceOpts, ref string) llb.State {
 		if fromClient != nil {
 			return *fromClient, nil
 		}
-		return llb.Image(ref, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraint(c)), nil
+
+		return llb.Image(ref, dalec.WithConstraint(c), llb.WithMetaResolver(sOpt.Resolver)), nil
path: frontend/build.go
user: login: DannyBrito
body: is there a reason why the arguments order was changed?
created_at: 2025-04-01T14:59:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,7 +155,8 @@ func GetBaseImage(sOpt dalec.SourceOpts, ref string) llb.State {
 		if fromClient != nil {
 			return *fromClient, nil
 		}
-		return llb.Image(ref, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraint(c)), nil
+
+		return llb.Image(ref, dalec.WithConstraint(c), llb.WithMetaResolver(sOpt.Resolver)), nil
path: frontend/build.go
user: login: cpuguy83
body: I think I had something else here that I removed that I wanted to make sure were applied after the passed in constraints.
created_at: 2025-04-02T15:51:18Z
author_association: COLLABORATOR
diff_hunk: @@ -51,24 +51,25 @@ func (c *Config) HandleBuildroot(ctx context.Context, client gwclient.Client) (*
 		}
 
 		pg := dalec.ProgressGroup("Setting up " + targetKey + " rpm buildroot: " + spec.Name)
-		sOpt, err := frontend.SourceOptFromClient(ctx, client)
+		sOpt, err := frontend.SourceOptFromClient(ctx, client, platform)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		worker, err := c.Worker(sOpt, pg)
+		pOpt := dalec.Platform(platform)
+		worker, err := c.Worker(sOpt, pg, pOpt)
path: targets/linux/rpm/distro/debug.go
user: login: cpuguy83
body: Changed.
created_at: 2025-04-02T16:12:04Z
author_association: COLLABORATOR
diff_hunk: @@ -142,6 +142,8 @@ jobs:
 
       - name: download deps
         run: go mod download
+      - name: Setup QEMU
+        run: docker run --rm --privileged upstream.azurecr.io/binfmt:latest --install all
path: .github/workflows/ci.yml
user: login: cpuguy83
body: Changed this.
created_at: 2025-04-02T16:12:15Z
author_association: COLLABORATOR
diff_hunk: @@ -142,6 +142,8 @@ jobs:
 
       - name: download deps
         run: go mod download
+      - name: Setup QEMU
+        run: docker run --rm --privileged tonistiigi/binfmt:latest --install all
path: .github/workflows/ci.yml
user: login: DannyBrito
body: I think docker updated the pull rate limit even lower. This might be a problem? https://docs.docker.com/docker-hub/usage/. Maybe the use of the upstream acr is fine?
created_at: 2025-04-02T16:23:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,6 +142,8 @@ jobs:
 
       - name: download deps
         run: go mod download
+      - name: Setup QEMU
+        run: docker run --rm --privileged tonistiigi/binfmt:latest --install all
path: .github/workflows/ci.yml
user: login: cpuguy83
body: We'll see if it becomes an issue.
It's not the only place we pull from dockerhub in the test suite.
created_at: 2025-04-02T16:25:27Z
author_association: COLLABORATOR
diff_hunk: @@ -117,7 +117,13 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 
 // Like [BuildWithPlatform] but with a pre-initialized dockerui.Client
 func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
-	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *dalec.DockerImageSpec, *dalec.DockerImageSpec, error) {
+	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (_ gwclient.Reference, _ *dalec.DockerImageSpec, _ *dalec.DockerImageSpec, retErr error) {
+		defer func() {
+			if r := recover(); r != nil {
+				retErr = fmt.Errorf("recovered panic in build: %+v", r)
path: frontend/build.go
user: login: pmengelbert
body: Is there ever a case where `retErr` could be non-nil when this deferred func runs?
```
				retErr = errors.Join(retErr, fmt.Errorf("recovered panic in build: %+v", r))
```
created_at: 2025-04-03T14:52:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,7 +117,13 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 
 // Like [BuildWithPlatform] but with a pre-initialized dockerui.Client
 func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
-	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *dalec.DockerImageSpec, *dalec.DockerImageSpec, error) {
+	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (_ gwclient.Reference, _ *dalec.DockerImageSpec, _ *dalec.DockerImageSpec, retErr error) {
+		defer func() {
+			if r := recover(); r != nil {
+				retErr = fmt.Errorf("recovered panic in build: %+v", r)
path: frontend/build.go
user: login: pmengelbert
body: As per offline discussion, this needs a stack trace. Can be deferred to another PR as long as a tracking issue is opened.
created_at: 2025-04-03T14:53:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,7 +117,13 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 
 // Like [BuildWithPlatform] but with a pre-initialized dockerui.Client
 func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
-	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *dalec.DockerImageSpec, *dalec.DockerImageSpec, error) {
+	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (_ gwclient.Reference, _ *dalec.DockerImageSpec, _ *dalec.DockerImageSpec, retErr error) {
+		defer func() {
+			if r := recover(); r != nil {
+				retErr = fmt.Errorf("recovered panic in build: %+v", r)
path: frontend/build.go
user: login: cpuguy83
body: If there's a panic, then yeah it would be non-nil.
created_at: 2025-04-03T15:04:46Z
author_association: COLLABORATOR
diff_hunk: @@ -117,7 +117,13 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 
 // Like [BuildWithPlatform] but with a pre-initialized dockerui.Client
 func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
-	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *dalec.DockerImageSpec, *dalec.DockerImageSpec, error) {
+	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (_ gwclient.Reference, _ *dalec.DockerImageSpec, _ *dalec.DockerImageSpec, retErr error) {
+		defer func() {
+			if r := recover(); r != nil {
+				retErr = fmt.Errorf("recovered panic in build: %+v", r)
path: frontend/build.go
user: login: cpuguy83
body: Sorry... I said that wrong... if there's a panic, then it would always be nil.
created_at: 2025-04-03T15:05:15Z
author_association: COLLABORATOR
diff_hunk: @@ -117,7 +117,13 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 
 // Like [BuildWithPlatform] but with a pre-initialized dockerui.Client
 func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
-	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *dalec.DockerImageSpec, *dalec.DockerImageSpec, error) {
+	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (_ gwclient.Reference, _ *dalec.DockerImageSpec, _ *dalec.DockerImageSpec, retErr error) {
+		defer func() {
+			if r := recover(); r != nil {
+				retErr = fmt.Errorf("recovered panic in build: %+v", r)
path: frontend/build.go
user: login: cpuguy83
body: Fixed.
created_at: 2025-04-03T15:17:55Z
author_association: COLLABORATOR
diff_hunk: @@ -115,9 +117,33 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 	return BuildWithPlatformFromUIClient(ctx, client, dc, f)
 }
 
+func getPanicStatck() error {
+	stackBuf := make([]uintptr, 32)
+	n := runtime.Callers(4, stackBuf) // Skip 4 frames to exclude runtime.Callers, the current function, and defer internals
+	stackBuf = stackBuf[:n]
+	frames := runtime.CallersFrames(stackBuf)
+	var stackTrace string
+	for {
+		frame, more := frames.Next()
+		stackTrace += fmt.Sprintf("%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
+		if !more {
+			break
+		}
+	}
+	return stderrors.New(stackTrace)
+}
+
 // Like [BuildWithPlatform] but with a pre-initialized dockerui.Client
 func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
-	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *dalec.DockerImageSpec, *dalec.DockerImageSpec, error) {
+	rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (_ gwclient.Reference, _ *dalec.DockerImageSpec, _ *dalec.DockerImageSpec, retErr error) {
+		defer func() {
+			if r := recover(); r != nil {
+				trace := getPanicStatck()
path: frontend/build.go
user: login: adamperlin
body: ```suggestion
				trace := getPanicStack()
```
created_at: 2025-04-03T17:21:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -115,9 +117,33 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 	return BuildWithPlatformFromUIClient(ctx, client, dc, f)
 }
 
+func getPanicStatck() error {
path: frontend/build.go
user: login: adamperlin
body: ```suggestion
func getPanicStack() error {
```
created_at: 2025-04-03T17:21:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -115,9 +117,33 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 	return BuildWithPlatformFromUIClient(ctx, client, dc, f)
 }
 
+func getPanicStatck() error {
path: frontend/build.go
user: login: cpuguy83
body: 😱 
created_at: 2025-04-03T18:18:44Z
author_association: COLLABORATOR
diff_hunk: @@ -115,9 +117,33 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
 	return BuildWithPlatformFromUIClient(ctx, client, dc, f)
 }
 
+func getPanicStatck() error {
path: frontend/build.go
user: login: cpuguy83
body: Fixed.
created_at: 2025-04-03T18:39:36Z
author_association: COLLABORATOR

PR comments:
user: login: pmengelbert
created_at: 2025-04-02T20:26:26Z
body: To clarify, is this PR intended to make Dalec capable of multiplatform images? Or is it intended to enable dalec to execute a cross-platform build *for a single platform*?
author_association: CONTRIBUTOR
user: login: pmengelbert
created_at: 2025-04-02T20:27:59Z
body: The reason I ask is that calling `SingleRef` on a `Result` with more than one platform causes a panic, IIRC. If this is intended to enale multiplatform, we'll need to change the references to `SingleRef`.
author_association: CONTRIBUTOR
user: login: cpuguy83
created_at: 2025-04-02T20:31:18Z
body: > To clarify, is this PR intended to make Dalec capable of multiplatform images? Or is it intended to enable dalec to execute a cross-platform build _for a single platform_?

It makes dalec build the provided platform.
Technically dalec can already build multiple platforms if they are provided, and that is handled higher up int he stack (frontend.BuildWIthPlatform deals with it)
author_association: COLLABORATOR
user: login: pmengelbert
created_at: 2025-04-02T21:03:17Z
body: > > To clarify, is this PR intended to make Dalec capable of multiplatform images? Or is it intended to enable dalec to execute a cross-platform build _for a single platform_?
> 
> It makes dalec build the provided platform. Technically dalec can already build multiple platforms if they are provided, and that is handled higher up int he stack (frontend.BuildWIthPlatform deals with it)

Definitely an edge case, but the `debug/gomods` target fails when multiple platforms are provided. Does that matter?

If it's not supported we should either document it or error out if it's requested. As of right now, there's a panic when the state is marshaled and no error message generated.
author_association: CONTRIBUTOR
user: login: pmengelbert
created_at: 2025-04-02T21:05:35Z
body: For the record, `debug/sources` and `debug/resolve` work when multiple platforms are specified.
author_association: CONTRIBUTOR
user: login: cpuguy83
created_at: 2025-04-02T23:00:11Z
body: > Definitely an edge case, but the debug/gomods target fails when multiple platforms are provided.

I cannot reproduce this.
Can you grab the stack trace from buildkit daemon logs (or dockerd logs if using that directly)?
author_association: COLLABORATOR
user: login: pmengelbert
created_at: 2025-04-03T01:13:51Z
body: > > Definitely an edge case, but the debug/gomods target fails when multiple platforms are provided.
> 
> I cannot reproduce this. Can you grab the stack trace from buildkit daemon logs (or dockerd logs if using that directly)?

When running `docker buildx build --platform=linux/arm64,linux/amd64 --build-arg=BUILDKIT_SYNTAX=local/dalec/frontend:latest -f tmp/moby-runc.yml --target=debug/gomods --output=/tmp/out .`

```
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] original dial target is: \"localhost\""
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1]Channel created"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1]parsed dial target is: resolver.Target{URL:url.URL{Scheme:\"passthrough\", Opaque:\"\", User:(*url.Userinfo)(nil), Host:\"\", Path:\"/localhost\", RawPath:\"\", OmitHost:false, ForceQuery:false, RawQuery:\"\", Fragment:\"\", RawFragment:\"\"}}"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1]Channel authority set to \"localhost\""
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1]Resolver state updated: {"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="  \"Addresses\": ["
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="    {"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="      \"Addr\": \"localhost\","
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="      \"ServerName\": \"\","
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="      \"Attributes\": null,"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="      \"BalancerAttributes\": null,"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="      \"Metadata\": null"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="    }"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="  ],"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="  \"Endpoints\": ["
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="    {"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="      \"Addresses\": ["
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="        {"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="          \"Addr\": \"localhost\","
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="          \"ServerName\": \"\","
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="          \"Attributes\": null,"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="          \"BalancerAttributes\": null,"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="          \"Metadata\": null"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="        }"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="      ],"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="      \"Attributes\": null"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="    }"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="  ],"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="  \"ServiceConfig\": null,"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="  \"Attributes\": null"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="} (resolver returned new addresses)"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1]Channel switches to new LB policy \"pick_first\""
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1 SubChannel #2]Subchannel created"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1]Channel Connectivity change to CONNECTING"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1]Channel exiting idle mode"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1 SubChannel #2]Subchannel Connectivity change to CONNECTING"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1 SubChannel #2]Subchannel picks a new address \"localhost\" to connect"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1 SubChannel #2]Subchannel Connectivity change to READY"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="2025/04/03 01:11:31 INFO: [core] [Channel #1]Channel Connectivity change to READY"
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Added handler to router" target=mariner2
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Added handler to router" target=azlinux3
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[azlinux3 mariner2 jammy]" targetKey=windowscross
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[azlinux3 mariner2 jammy]" targetKey=bionic
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[mariner2 jammy azlinux3]" targetKey=focal
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Added handler to router" target=jammy
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[azlinux3 mariner2 jammy]" targetKey=noble
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[jammy azlinux3 mariner2]" targetKey=bookworm
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[azlinux3 mariner2 jammy]" targetKey=bullseye
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[azlinux3 mariner2 jammy]" targetKey=almalinux8
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[mariner2 jammy azlinux3]" targetKey=almalinux9
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[azlinux3 mariner2 jammy]" targetKey=rockylinux9
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Target not in the spec, skipping" spec targets="[mariner2 jammy azlinux3]" targetKey=rockylinux8
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Handling request" handlers="[debug mariner2 azlinux3 jammy]" requestid= target=debug/gomods targetKey=
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Using prefix match for target" handlers="[debug mariner2 azlinux3 jammy]" matching request=debug/gomods prefix=debug requestid= target=debug/gomods targetKey=
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Added handler to router" target=resolve
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Added handler to router" target=sources
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Added handler to router" target=patched-sources
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Added handler to router" target=gomods
Apr 02 21:11:31 thinkpad dockerd[211064]: time="2025-04-03T01:11:31Z" level=info msg="Handling request" handlers="[resolve sources patched-sources gomods]" matched=debug requestid= target=gomods targetKey=debug
Apr 02 21:11:31 thinkpad dockerd[211064]: panic: runtime error: invalid memory address or nil pointer dereference
Apr 02 21:11:31 thinkpad dockerd[211064]: [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xba74dc]
Apr 02 21:11:31 thinkpad dockerd[211064]: goroutine 16 [running]:
Apr 02 21:11:31 thinkpad dockerd[211064]: github.com/Azure/dalec/frontend/debug.Gomods.func1({0xf1fbf0, 0xc0000d6e10}, {0xf24050, 0xc0000ffb90}, 0xc00042bf20, 0xc00032aa00, {0x0?, 0x422fa5?})
Apr 02 21:11:31 thinkpad dockerd[211064]:         github.com/Azure/dalec/frontend/debug/handle_gomod.go:43 +0x35c
Apr 02 21:11:31 thinkpad dockerd[211064]: github.com/Azure/dalec/frontend.BuildWithPlatformFromUIClient.func1({0xf1fbf0, 0xc0000d6e10}, 0xc00042bf20, 0xc000297c08?)
Apr 02 21:11:31 thinkpad dockerd[211064]:         github.com/Azure/dalec/frontend/build.go:127 +0x115
Apr 02 21:11:31 thinkpad dockerd[211064]: github.com/moby/buildkit/frontend/dockerui.(*Client).Build.func1()
Apr 02 21:11:31 thinkpad dockerd[211064]:         github.com/moby/buildkit@v0.20.1/frontend/dockerui/build.go:39 +0x71
Apr 02 21:11:31 thinkpad dockerd[211064]: golang.org/x/sync/errgroup.(*Group).Go.func1()
Apr 02 21:11:31 thinkpad dockerd[211064]:         golang.org/x/sync@v0.12.0/errgroup/errgroup.go:78 +0x50
Apr 02 21:11:31 thinkpad dockerd[211064]: created by golang.org/x/sync/errgroup.(*Group).Go in goroutine 1
Apr 02 21:11:31 thinkpad dockerd[211064]:         golang.org/x/sync@v0.12.0/errgroup/errgroup.go:75 +0x96
Apr 02 21:11:31 thinkpad dockerd[93726]: time="2025-04-02T21:11:31.490200273-04:00" level=warning msg="failed to read oom_kill event" error="open /sys/fs/cgroup/system.slice:docker:cer4i064b9dtpkt5k42r9khxt/memory.events: no such file or directory" spanID=a5a0999073818eb7 traceID=7b16f8ad08a79b99d728d1c93aaa8e7a
Apr 02 21:11:31 thinkpad dockerd[93726]: time="2025-04-02T21:11:31.523850246-04:00" level=error msg=/moby.buildkit.v1.frontend.LLBBridge/Solve error="rpc error: code = Unknown desc = failed with build-arg:BUILDKIT_SYNTAX = local/dalec/frontend:latest: exit code: 2" spanID=a5a0999073818eb7 traceID=7b16f8ad08a79b99d728d1c93aaa8e7a
Apr 02 21:11:31 thinkpad dockerd[93726]: time="2025-04-02T21:11:31.540425680-04:00" level=error msg=/moby.buildkit.v1.Control/Solve error="rpc error: code = Unknown desc = failed with build-arg:BUILDKIT_SYNTAX = local/dalec/frontend:latest: exit code: 2" spanID=f8efc1cc407e2ea0 traceID=aadc75097e75d7ff4e3a52273703360d
```
author_association: CONTRIBUTOR
user: login: pmengelbert
created_at: 2025-04-03T14:18:33Z
body: This problem appears unrelated. I can't build `test/fixtures/moby-runc.yml` even on a single platform.
author_association: CONTRIBUTOR
user: login: cpuguy83
created_at: 2025-04-03T14:25:35Z
body: On a positive note, I realize why not all panics are being recovered.
author_association: COLLABORATOR
user: login: cpuguy83
created_at: 2025-04-03T14:38:49Z
body: For posterity, it looks like this is an existing bug triggered in the debug/gomods target when the spec does not specify any gomods.
author_association: COLLABORATOR
user: login: cpuguy83
created_at: 2025-04-03T16:39:12Z
body: Just pushed a change to undo my docker-bake changes that let me check for that panic more easily. Didn't mean to push that.
author_association: COLLABORATOR