Repository: openllb/hlb
Pull request #307 (2022-02-21 21:37:51)

Add support for stargz images
See: https://github.com/moby/buildkit/blob/master/docs/stargz-estargz.md

Example manifest:
```json
{
   "mediaType": "application/vnd.oci.image.manifest.v1+json",
   "schemaVersion": 2,
   "config": {
      "mediaType": "application/vnd.oci.image.config.v1+json",
      "digest": "sha256:cba6a63f22c24075f2f0c9efeea9603dc9845aa3afcec9cd061c08f5d12ecc75",
      "size": 1031
   },
   "layers": [
      {
         "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
         "digest": "sha256:25af8f56fba7da444a7be3ff6a6df9ad9c15b19c624e1c2338bb0d1b1b226f69",
         "size": 314668980
      },
      {
         "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
         "digest": "sha256:7c084638d9c588cc1efc79c73a44e1424899eb10b4067119393a0dfab8400876",
         "size": 485,
         "annotations": {
            "containerd.io/snapshot/stargz/toc.digest": "sha256:c859b4593016fd4de87ba9dd20076c800428dc2f4b3b8b5e2b3ba1dc5d25f6d3",
            "io.containers.estargz.uncompressed-size": "4096"
         }
      }
   ]
}
```

hinshun: Pending discussion in: https://github.com/moby/buildkit/issues/2663
hinshun: BuildKit's stargz compression default behavior is to leave standard layers alone, creating a mixed mode image where some layers are standard and some are stargz. I believe this is intuitive for users, so in the spirit of HLB, we should always end up with a fully stargz image, and have options available for mixed mode later.

See: https://github.com/moby/buildkit/issues/2663#issuecomment-1047300669
hinshun: Also it seems that gateway exec unlazy stargz image, looking at our gateway code, nothing sticks out on our usage.

See: https://github.com/moby/buildkit/issues/2664
hinshun: Added tests and disabled for moby engine until it supports stargz.

Patch:
diff --git a/build.hlb b/build.hlb
index 6d9004a..08fc021 100644
--- a/build.hlb
+++ b/build.hlb
@@ -35,5 +35,5 @@ fs publishDocs() {
 }
 
 fs src() {
-	local "." with includePatterns("**/*.go", "go.mod", "go.sum", ".golangci.yml", ".git")
+	local "." with includePatterns("**/*.go", "go.mod", "go.sum", ".golangci.yml", ".git", "**/*.json")
 }
diff --git a/builtin/lookup.go b/builtin/lookup.go
index f3fff58..1e71222 100644
--- a/builtin/lookup.go
+++ b/builtin/lookup.go
@@ -265,6 +265,14 @@ var (
 					},
 				},
 			},
+			"option::dockerPush": {
+				Func: map[string]FuncLookup{
+					"stargz": {
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
+					},
+				},
+			},
 			"option::frontend": {
 				Func: map[string]FuncLookup{
 					"input": {
@@ -1185,6 +1193,16 @@ fs diff(fs base)
 # @return an option to push the filesystem to a registry.
 fs dockerPush(string ref) binds (string digest)
 
+# Compress the image as a eStargz image before pushing.
+#
+# eStargz is a lazily pullable image format. It is compatible to OCI/Docker
+# images so it can be pushed to standard container registries and runnable on
+# eStargz-agnostic runtimes like Docker.
+# See: https://github.com/containerd/stargz-snapshotter
+#
+# @return an option to compress image as eStargz before pushing.
+option::dockerPush stargz()
+
 # Loads the filesystem as a Docker image to the docker client found in your
 # environment.
 #
diff --git a/checker/checker.go b/checker/checker.go
index 54425fb..d4ca447 100644
--- a/checker/checker.go
+++ b/checker/checker.go
@@ -274,13 +274,13 @@ func (c *checker) CheckReferences(mod *ast.Module, name string) error {
 			}
 		},
 		func(block *ast.BlockStmt, callStmt *ast.CallStmt, callExpr *ast.CallExpr) {
-			err := c.checkNestedCallExpr(block.Scope, callStmt.Name, callStmt.Args, callStmt.Sig, callStmt.WithClause, callExpr, name)
+			err := c.checkNestedCallExpr(block.Scope, callStmt.Name, callStmt.Args, callStmt.Signature, callStmt.WithClause, callExpr, name)
 			if err != nil {
 				c.err(err)
 			}
 		},
 		func(block *ast.BlockStmt, parentCallExpr, callExpr *ast.CallExpr) {
-			err := c.checkNestedCallExpr(block.Scope, parentCallExpr.Name, parentCallExpr.Arguments(), parentCallExpr.Sig, nil, callExpr, name)
+			err := c.checkNestedCallExpr(block.Scope, parentCallExpr.Name, parentCallExpr.Args(), parentCallExpr.Signature, nil, callExpr, name)
 			if err != nil {
 				c.err(err)
 			}
@@ -391,7 +391,7 @@ func (c *checker) checkCallStmt(scope *ast.Scope, kset *ast.KindSet, call *ast.C
 	for _, field := range signature {
 		kinds = append(kinds, field.Kind())
 	}
-	call.Sig = kinds
+	call.Signature = kinds
 	return nil
 }
 
@@ -399,7 +399,7 @@ func (c *checker) checkCallExpr(scope *ast.Scope, kset *ast.KindSet, call *ast.C
 	if call.Breakpoint() {
 		return nil
 	}
-	signature, err := c.checkCall(scope, kset, call.Name, call.Arguments(), nil)
+	signature, err := c.checkCall(scope, kset, call.Name, call.Args(), nil)
 	if err != nil {
 		return err
 	}
@@ -407,7 +407,7 @@ func (c *checker) checkCallExpr(scope *ast.Scope, kset *ast.KindSet, call *ast.C
 	for _, field := range signature {
 		kinds = append(kinds, field.Kind())
 	}
-	call.Sig = kinds
+	call.Signature = kinds
 	return nil
 }
 
diff --git a/codegen/builtin.go b/codegen/builtin.go
index 1c85f4b..46da6a8 100644
--- a/codegen/builtin.go
+++ b/codegen/builtin.go
@@ -148,6 +148,9 @@ var (
 		"option::manifest": {
 			"platform": Platform{},
 		},
+		"option::dockerPush": {
+			"stargz": Stargz{},
+		},
 	}
 )
 
diff --git a/codegen/builtin_fs.go b/codegen/builtin_fs.go
index de03a05..3c185cb 100644
--- a/codegen/builtin_fs.go
+++ b/codegen/builtin_fs.go
@@ -12,6 +12,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/containerd/containerd/platforms"
+	"github.com/containerd/containerd/remotes/docker"
 	"github.com/docker/buildx/util/imagetools"
 	"github.com/docker/buildx/util/progress"
 	"github.com/docker/distribution/reference"
@@ -28,6 +30,7 @@ import (
 	"github.com/openllb/hlb/local"
 	"github.com/openllb/hlb/parser"
 	"github.com/openllb/hlb/pkg/llbutil"
+	"github.com/openllb/hlb/pkg/stargzutil"
 	"github.com/openllb/hlb/solver"
 	"github.com/pkg/errors"
 	fstypes "github.com/tonistiigi/fsutil/types"
@@ -60,8 +63,13 @@ func (s Scratch) Call(ctx context.Context, cln *client.Client, val Value, opts O
 type Image struct{}
 
 func (i Image) Call(ctx context.Context, cln *client.Client, val Value, opts Option, ref string) (Value, error) {
+	fs, err := val.Filesystem()
+	if err != nil {
+		return nil, err
+	}
+
 	var imageOpts []llb.ImageOption
-	platform := DefaultPlatform(ctx)
+	platform := fs.Platform
 	for _, opt := range opts {
 		switch o := opt.(type) {
 		case llb.ImageOption:
@@ -72,9 +80,6 @@ func (i Image) Call(ctx context.Context, cln *client.Client, val Value, opts Opt
 	}
 	imageOpts = append(imageOpts, llb.Platform(platform))
 
-	resolveOpt := llb.ResolveImageConfigOpt{
-		Platform: &platform,
-	}
 	for _, opt := range SourceMap(ctx) {
 		imageOpts = append(imageOpts, opt)
 	}
@@ -86,13 +91,24 @@ func (i Image) Call(ctx context.Context, cln *client.Client, val Value, opts Opt
 	ref = reference.TagNameOnly(named).String()
 
 	var (
-		st       = llb.Image(ref, imageOpts...)
-		image    = &solver.ImageSpec{}
-		resolver = ImageResolver(ctx)
+		st         = llb.Image(ref, imageOpts...)
+		image      = &solver.ImageSpec{}
+		resolver   = ImageResolver(ctx)
+		resolveOpt = llb.ResolveImageConfigOpt{
+			Platform: &platform,
+			// For some reason, llb.ResolveModeDefault defaults to
+			// llb.ResolveModeForcePull on BuildKit but it defaults to
+			// llb.ResolveModePreferLocal on docker engine, so we just set our own.
+			ResolveMode: llb.ResolveModeForcePull.String(),
+		}
 	)
-
 	if resolver != nil {
-		_, config, err := resolver.ResolveImageConfig(ctx, ref, resolveOpt)
+		dgst, config, err := resolver.ResolveImageConfig(ctx, ref, resolveOpt)
+		if err != nil {
+			return nil, Arg(ctx, 0).WithError(err)
+		}
+
+		image.Canonical, err = reference.WithDigest(named, dgst)
 		if err != nil {
 			return nil, Arg(ctx, 0).WithError(err)
 		}
@@ -108,16 +124,20 @@ func (i Image) Call(ctx context.Context, cln *client.Client, val Value, opts Opt
 		}
 	}
 
-	return NewValue(ctx, Filesystem{
-		State:    st,
-		Image:    image,
-		Platform: platform,
-	})
+	fs.State = st
+	fs.Image = image
+	fs.Platform = platform
+	return NewValue(ctx, fs)
 }
 
 type HTTP struct{}
 
 func (h HTTP) Call(ctx context.Context, cln *client.Client, val Value, opts Option, url string) (Value, error) {
+	fs, err := val.Filesystem()
+	if err != nil {
+		return nil, err
+	}
+
 	var httpOpts []llb.HTTPOption
 	for _, opt := range opts {
 		switch o := opt.(type) {
@@ -129,12 +149,18 @@ func (h HTTP) Call(ctx context.Context, cln *client.Client, val Value, opts Opti
 		httpOpts = append(httpOpts, opt)
 	}
 
-	return NewValue(ctx, llb.HTTP(url, httpOpts...))
+	fs.State = llb.HTTP(url, httpOpts...)
+	return NewValue(ctx, fs)
 }
 
 type Git struct{}
 
 func (g Git) Call(ctx context.Context, cln *client.Client, val Value, opts Option, remote, ref string) (Value, error) {
+	fs, err := val.Filesystem()
+	if err != nil {
+		return nil, err
+	}
+
 	var gitOpts []llb.GitOption
 	for _, opt := range opts {
 		switch o := opt.(type) {
@@ -146,13 +172,19 @@ func (g Git) Call(ctx context.Context, cln *client.Client, val Value, opts Optio
 		gitOpts = append(gitOpts, opt)
 	}
 
-	return NewValue(ctx, llb.Git(remote, ref, gitOpts...))
+	fs.State = llb.Git(remote, ref, gitOpts...)
+	return NewValue(ctx, fs)
 }
 
 type Local struct{}
 
 func (l Local) Call(ctx context.Context, cln *client.Client, val Value, opts Option, localPath string) (Value, error) {
-	localPath, err := parser.ResolvePath(ModuleDir(ctx), localPath)
+	fs, err := val.Filesystem()
+	if err != nil {
+		return nil, err
+	}
+
+	localPath, err = parser.ResolvePath(ModuleDir(ctx), localPath)
 	if err != nil {
 		return nil, err
 	}
@@ -204,10 +236,7 @@ func (l Local) Call(ctx context.Context, cln *client.Client, val Value, opts Opt
 		localOpts = append(localOpts, llb.SessionID(sessionID))
 	}
 
-	fs := Filesystem{
-		State:    llb.Local(localPath, localOpts...),
-		Platform: DefaultPlatform(ctx),
-	}
+	fs.State = llb.Local(localPath, localOpts...)
 	fs.SessionOpts = append(fs.SessionOpts, llbutil.WithSyncedDir(id, filesync.SyncedDir{
 		Name: localPath,
 		Dir:  localDir,
@@ -224,6 +253,11 @@ func (l Local) Call(ctx context.Context, cln *client.Client, val Value, opts Opt
 type Frontend struct{}
 
 func (f Frontend) Call(ctx context.Context, cln *client.Client, val Value, opts Option, source string) (Value, error) {
+	fs, err := val.Filesystem()
+	if err != nil {
+		return nil, err
+	}
+
 	named, err := reference.ParseNormalizedNamed(source)
 	if err != nil {
 		return nil, errdefs.WithInvalidImageRef(err, Arg(ctx, 0), source)
@@ -260,11 +294,6 @@ func (f Frontend) Call(ctx context.Context, cln *client.Client, val Value, opts
 
 	g, ctx := errgroup.WithContext(ctx)
 
-	fs, err := ZeroValue(ctx).Filesystem()
-	if err != nil {
-		return nil, err
-	}
-
 	g.Go(func() error {
 		return s.Run(ctx, cln.Dialer())
 	})
@@ -700,11 +729,32 @@ func (dp DockerPush) Call(ctx context.Context, cln *client.Client, val Value, op
 			return nil
 		}),
 	)
+
+	stargz := false
 	for _, opt := range opts {
 		switch o := opt.(type) {
 		case solver.SolveOption:
 			exportFS.SolveOpts = append(exportFS.SolveOpts, o)
+		case *Stargz:
+			stargz = true
+		}
+	}
+
+	if stargz {
+		// Regular layers are also allowed in stargz images. By default, base layers
+		// that weren't pulled (lazy) are not converted to stargz. However, we want
+		// the default behaviour to ensure the final layers to be all stargz so that
+		// its more predictable. Future work could allow advanced users to choose to
+		// keep mixed layers.
+		forceCompression := false
+		if exportFS.Image.Canonical != nil {
+			resolver := docker.NewResolver(docker.ResolverOptions{})
+			forceCompression, err = stargzutil.HasNonStargzLayer(ctx, resolver, platforms.Only(exportFS.Platform), exportFS.Image.Canonical.String())
+			if err != nil {
+				return nil, err
+			}
 		}
+		exportFS.SolveOpts = append(exportFS.SolveOpts, solver.WithStargz(forceCompression))
 	}
 
 	dockerAPI := DockerAPI(ctx)
diff --git a/codegen/builtin_option.go b/codegen/builtin_option.go
index cb2e80c..cbfb60b 100644
--- a/codegen/builtin_option.go
+++ b/codegen/builtin_option.go
@@ -897,3 +897,19 @@ func (p Platform) Call(ctx context.Context, cln *client.Client, val Value, opts
 		Architecture: arch,
 	}))
 }
+
+type Stargz struct{}
+
+func (s Stargz) Call(ctx context.Context, cln *client.Client, val Value, opts Option) (Value, error) {
+	dockerAPI := DockerAPI(ctx)
+	if dockerAPI.Moby {
+		return nil, errdefs.WithDockerEngineUnsupported(ProgramCounter(ctx))
+	}
+
+	retOpts, err := val.Option()
+	if err != nil {
+		return nil, err
+	}
+
+	return NewValue(ctx, append(retOpts, &Stargz{}))
+}
diff --git a/codegen/codegen.go b/codegen/codegen.go
index d0a49b6..81facf1 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/lithammer/dedent"
 	"github.com/moby/buildkit/client"
-	"github.com/moby/buildkit/util/flightcontrol"
 	"github.com/openllb/hlb/checker"
 	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/errdefs"
@@ -25,7 +24,6 @@ type CodeGen struct {
 	cln      *client.Client
 	resolver Resolver
 	dbgr     *debugger
-	g        flightcontrol.Group
 }
 
 func New(cln *client.Client, resolver Resolver) *CodeGen {
@@ -83,7 +81,7 @@ func (cg *CodeGen) Generate(ctx context.Context, mod *ast.Module, targets []Targ
 	return solver.Parallel(requests...), nil
 }
 
-func (cg *CodeGen) EmitExpr(ctx context.Context, scope *ast.Scope, expr *ast.Expr, opts Option, b *ast.Binding, ret Register) error {
+func (cg *CodeGen) EmitExpr(ctx context.Context, scope *ast.Scope, expr *ast.Expr, args []Value, opts Option, b *ast.Binding, ret Register) error {
 	ctx = WithProgramCounter(ctx, expr)
 
 	switch {
@@ -92,27 +90,26 @@ func (cg *CodeGen) EmitExpr(ctx context.Context, scope *ast.Scope, expr *ast.Exp
 	case expr.BasicLit != nil:
 		return cg.EmitBasicLit(ctx, scope, expr.BasicLit, ret)
 	case expr.CallExpr != nil:
-		ret.SetAsync(func(val Value) (Value, error) {
+		return ret.SetAsync(func(v Value) (Value, error) {
 			if expr.CallExpr.Breakpoint() {
 				var err error
 				if cg.dbgr != nil {
 					ctx = WithFrame(ctx, Frame{expr.CallExpr.Name})
-					err = cg.dbgr.yield(ctx, scope, expr.CallExpr, val, nil, nil)
+					err = cg.dbgr.yield(ctx, scope, expr.CallExpr, v, nil, nil)
 				}
-				return val, err
+				return v, err
 			}
 
-			err := cg.lookupCall(ctx, scope, expr.CallExpr.Ident())
+			err := cg.lookupCall(ctx, scope, expr.CallExpr.Name.Ident)
 			if err != nil {
 				return nil, err
 			}
 
 			ret := NewRegister(ctx)
-			ret.Set(val)
+			ret.Set(v)
 			err = cg.EmitCallExpr(ctx, scope, expr.CallExpr, ret)
 			return ret.Value(), err
 		})
-		return nil
 	default:
 		return errdefs.WithInternalErrorf(expr, "invalid expr")
 	}
@@ -160,7 +157,7 @@ func (cg *CodeGen) EmitStringLit(ctx context.Context, scope *ast.Scope, str *ast
 			}
 		case f.Interpolated != nil:
 			exprRet := NewRegister(ctx)
-			err := cg.EmitExpr(ctx, scope, f.Interpolated.Expr, nil, nil, exprRet)
+			err := cg.EmitExpr(ctx, scope, f.Interpolated.Expr, nil, nil, nil, exprRet)
 			if err != nil {
 				return err
 			}
@@ -193,7 +190,7 @@ func (cg *CodeGen) EmitHeredoc(ctx context.Context, scope *ast.Scope, heredoc *a
 			}
 		case f.Interpolated != nil:
 			exprRet := NewRegister(ctx)
-			err := cg.EmitExpr(ctx, scope, f.Interpolated.Expr, nil, nil, exprRet)
+			err := cg.EmitExpr(ctx, scope, f.Interpolated.Expr, nil, nil, nil, exprRet)
 			if err != nil {
 				return err
 			}
@@ -249,14 +246,17 @@ func (cg *CodeGen) EmitRawHeredoc(ctx context.Context, scope *ast.Scope, heredoc
 }
 
 func (cg *CodeGen) EmitCallExpr(ctx context.Context, scope *ast.Scope, call *ast.CallExpr, ret Register) error {
-	// Evaluate args first.
-	args := cg.Evaluate(ctx, scope, call, nil)
-	for i, arg := range call.Arguments() {
+	args, err := cg.Evaluate(ctx, scope, nil, call.Signature, call.Args()...)
+	if err != nil {
+		return err
+	}
+	for i, arg := range call.Args() {
 		ctx = WithArg(ctx, i, arg)
 	}
 
-	// Yield before executing call expression.
 	ctx = WithFrame(ctx, Frame{call.Name})
+
+	// Yield before executing call expression.
 	if cg.dbgr != nil {
 		err := cg.dbgr.yield(ctx, scope, call, ret.Value(), nil, nil)
 		if err != nil {
@@ -267,7 +267,7 @@ func (cg *CodeGen) EmitCallExpr(ctx context.Context, scope *ast.Scope, call *ast
 	return cg.EmitIdentExpr(ctx, scope, call.Name, call.Name.Ident, args, nil, nil, ret)
 }
 
-func (cg *CodeGen) EmitIdentExpr(ctx context.Context, scope *ast.Scope, ie *ast.IdentExpr, lookup *ast.Ident, args []Register, opts Register, b *ast.Binding, ret Register) error {
+func (cg *CodeGen) EmitIdentExpr(ctx context.Context, scope *ast.Scope, ie *ast.IdentExpr, lookup *ast.Ident, args []Value, opts Option, b *ast.Binding, ret Register) error {
 	ctx = WithProgramCounter(ctx, ie)
 
 	obj := scope.Lookup(lookup.Text)
@@ -277,10 +277,9 @@ func (cg *CodeGen) EmitIdentExpr(ctx context.Context, scope *ast.Scope, ie *ast.
 
 	switch n := obj.Node.(type) {
 	case *ast.BuiltinDecl:
-		ret.SetAsync(func(val Value) (Value, error) {
-			return cg.EmitBuiltinDecl(ctx, scope, n, args, opts, b, val)
+		return ret.SetAsync(func(v Value) (Value, error) {
+			return cg.EmitBuiltinDecl(ctx, scope, n, args, opts, b, v)
 		})
-		return nil
 	case *ast.FuncDecl:
 		return cg.EmitFuncDecl(ctx, n, args, nil, ret)
 	case *ast.BindClause:
@@ -292,27 +291,23 @@ func (cg *CodeGen) EmitIdentExpr(ctx context.Context, scope *ast.Scope, ie *ast.
 		}
 		return cg.EmitIdentExpr(ctx, imod.Scope, ie, ie.Reference.Ident, args, opts, nil, ret)
 	case *ast.Field:
-		dret, ok := obj.Data.(Register)
-		if !ok {
-			return errdefs.WithInternalErrorf(ProgramCounter(ctx), "expected register on field")
+		val, err := NewValue(ctx, obj.Data)
+		if err != nil {
+			return err
 		}
-		dval := dret.Value()
-
-		ret.SetAsync(func(val Value) (Value, error) {
-			if dval.Kind() != ast.Option || val.Kind() != ast.Option {
-				return dval, nil
-			}
-			retOpts, err := val.Option()
+		if val.Kind() != ast.Option || ret.Value().Kind() != ast.Option {
+			return ret.Set(val)
+		} else {
+			retOpts, err := ret.Value().Option()
 			if err != nil {
-				return nil, err
+				return err
 			}
-			valOpts, err := dval.Option()
+			valOpts, err := val.Option()
 			if err != nil {
-				return nil, err
+				return err
 			}
-			return NewValue(ctx, append(retOpts, valOpts...))
-		})
-		return nil
+			return ret.Set(append(retOpts, valOpts...))
+		}
 	default:
 		return errdefs.WithInternalErrorf(n, "invalid resolved object")
 	}
@@ -323,7 +318,7 @@ func (cg *CodeGen) EmitImport(ctx context.Context, mod *ast.Module, id *ast.Impo
 	ctx = WithReturnType(ctx, ast.None)
 
 	ret := NewRegister(ctx)
-	err = cg.EmitExpr(ctx, mod.Scope, id.Expr, nil, nil, ret)
+	err = cg.EmitExpr(ctx, mod.Scope, id.Expr, nil, nil, nil, ret)
 	if err != nil {
 		return
 	}
@@ -386,7 +381,7 @@ func (cg *CodeGen) EmitImport(ctx context.Context, mod *ast.Module, id *ast.Impo
 	return
 }
 
-func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *ast.Scope, bd *ast.BuiltinDecl, args []Register, opts Register, b *ast.Binding, val Value) (Value, error) {
+func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *ast.Scope, bd *ast.BuiltinDecl, args []Value, opts Option, b *ast.Binding, val Value) (Value, error) {
 	var callable interface{}
 	if ReturnType(ctx) != ast.None {
 		callable = Callables[ReturnType(ctx)][bd.Name]
@@ -408,23 +403,13 @@ func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *ast.Scope, bd *as
 		ctx = WithBinding(ctx, b)
 	}
 
-	// Get value of options register.
-	var opt Option
-	if opts != nil {
-		var err error
-		opt, err = opts.Value().Option()
-		if err != nil {
-			return nil, err
-		}
-	}
-
 	var (
 		c   = reflect.ValueOf(callable).MethodByName("Call")
 		ins = []reflect.Value{
 			reflect.ValueOf(ctx),
 			reflect.ValueOf(cg.cln),
 			reflect.ValueOf(val),
-			reflect.ValueOf(opt),
+			reflect.ValueOf(opts),
 		}
 	)
 
@@ -439,19 +424,13 @@ func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *ast.Scope, bd *as
 		return nil, errdefs.WithInternalErrorf(ProgramCounter(ctx), "`%s` expected %d args, got %d", bd, expected, len(args))
 	}
 
-	// Get value of args registers.
-	vals := make([]Value, len(args))
-	for i, arg := range args {
-		vals[i] = arg.Value()
-	}
-
 	// Reflect regular arguments.
 	for i := len(PrototypeIn); i < numIn; i++ {
 		var (
 			param = c.Type().In(i)
-			val   = vals[i-len(PrototypeIn)]
+			arg   = args[i-len(PrototypeIn)]
 		)
-		rval, err := val.Reflect(param)
+		rval, err := arg.Reflect(param)
 		if err != nil {
 			return nil, err
 		}
@@ -460,9 +439,9 @@ func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *ast.Scope, bd *as
 
 	// Reflect variadic arguments.
 	if c.Type().IsVariadic() {
-		for i := numIn - len(PrototypeIn); i < len(vals); i++ {
+		for i := numIn - len(PrototypeIn); i < len(args); i++ {
 			param := c.Type().In(numIn).Elem()
-			rval, err := vals[i].Reflect(param)
+			rval, err := args[i].Reflect(param)
 			if err != nil {
 				return nil, err
 			}
@@ -490,7 +469,7 @@ func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *ast.Scope, bd *as
 	return outs[0].Interface().(Value), nil
 }
 
-func (cg *CodeGen) EmitFuncDecl(ctx context.Context, fd *ast.FuncDecl, args []Register, b *ast.Binding, ret Register) error {
+func (cg *CodeGen) EmitFuncDecl(ctx context.Context, fd *ast.FuncDecl, args []Value, b *ast.Binding, ret Register) error {
 	if fd.Body == nil {
 		return nil
 	}
@@ -533,7 +512,7 @@ func (cg *CodeGen) EmitFuncDecl(ctx context.Context, fd *ast.FuncDecl, args []Re
 	return cg.EmitBlock(ctx, scope, fd.Body, b, ret)
 }
 
-func (cg *CodeGen) EmitBinding(ctx context.Context, b *ast.Binding, args []Register, ret Register) error {
+func (cg *CodeGen) EmitBinding(ctx context.Context, b *ast.Binding, args []Value, ret Register) error {
 	return cg.EmitFuncDecl(ctx, b.Bind.Closure, args, b, ret)
 }
 
@@ -545,26 +524,19 @@ func (cg *CodeGen) lookupCall(ctx context.Context, scope *ast.Scope, lookup *ast
 
 	switch n := obj.Node.(type) {
 	case *ast.ImportDecl:
-		// De-duplicate import resolution using a flightcontrol group key'ed by the
-		// import decl's filename + line + column position, which is unique per
-		// import. FS de-duplication should be handled by codegen cache.
-		key := parser.FormatPos(n.Pos)
-		_, err := cg.g.Do(ctx, key, func(ctx context.Context) (interface{}, error) {
-			_, ok := obj.Data.(*ast.Module)
-			if ok {
-				return nil, nil
-			}
+		_, ok := obj.Data.(*ast.Module)
+		if ok {
+			break
+		}
 
-			mod := scope.ByLevel(ast.ModuleScope).Node.(*ast.Module)
-			imod, _, err := cg.EmitImport(ctx, mod, n)
-			if err != nil {
-				return nil, err
-			}
-			obj.Data = imod
+		mod := scope.ByLevel(ast.ModuleScope).Node.(*ast.Module)
+		imod, _, err := cg.EmitImport(ctx, mod, n)
+		if err != nil {
+			return err
+		}
+		obj.Data = imod
 
-			err = checker.CheckReferences(mod, n.Name.Text)
-			return nil, err
-		})
+		err = checker.CheckReferences(mod, n.Name.Text)
 		if err != nil {
 			return err
 		}
@@ -585,28 +557,28 @@ func (cg *CodeGen) EmitBlock(ctx context.Context, scope *ast.Scope, block *ast.B
 		var err error
 		switch {
 		case stmt.Call != nil:
-			ret.SetAsync(func(val Value) (Value, error) {
+			err = ret.SetAsync(func(v Value) (Value, error) {
 				if stmt.Call.Breakpoint() {
 					var err error
 					if cg.dbgr != nil {
 						ctx = WithFrame(ctx, Frame{stmt.Call.Name})
-						err = cg.dbgr.yield(ctx, scope, stmt.Call, val, nil, nil)
+						err = cg.dbgr.yield(ctx, scope, stmt.Call, v, nil, nil)
 					}
-					return val, err
+					return v, err
 				}
 
-				err := cg.lookupCall(ctx, scope, stmt.Call.Ident())
+				err := cg.lookupCall(ctx, scope, stmt.Call.Name.Ident)
 				if err != nil {
 					return nil, err
 				}
 
 				ret := NewRegister(ctx)
-				ret.Set(val)
+				ret.Set(v)
 				err = cg.EmitCallStmt(ctx, scope, stmt.Call, b, ret)
 				return ret.Value(), err
 			})
 		case stmt.Expr != nil:
-			err = cg.EmitExpr(ctx, scope, stmt.Expr.Expr, nil, b, ret)
+			err = cg.EmitExpr(ctx, scope, stmt.Expr.Expr, nil, nil, b, ret)
 		default:
 			return errdefs.WithInternalErrorf(stmt, "invalid stmt")
 		}
@@ -619,42 +591,45 @@ func (cg *CodeGen) EmitBlock(ctx context.Context, scope *ast.Scope, block *ast.B
 }
 
 func (cg *CodeGen) EmitCallStmt(ctx context.Context, scope *ast.Scope, call *ast.CallStmt, b *ast.Binding, ret Register) error {
-	// Evaluate with block first.
-	opts := NewRegister(ctx)
-	if call.WithClause != nil {
-		scope, expr := scope, call.WithClause.Expr
-		opts.SetAsync(func(Value) (Value, error) {
-			// If with clause is a call expr, still wrap the scope as if it was a single
-			// element option block.
-			if expr.CallExpr != nil {
-				scope = ast.NewScope(scope, ast.BlockScope, expr.CallExpr)
-			}
-
-			ctx := WithProgramCounter(ctx, expr)
-			ctx = WithReturnType(ctx, ast.Kind(fmt.Sprintf("%s::%s", ast.Option, call.Name)))
-
-			// WithClause provides option expressions access to the binding.
-			ret := NewRegister(ctx)
-			err := cg.EmitExpr(ctx, scope, expr, nil, b, ret)
-			return ret.Value(), err
-		})
+	args, err := cg.Evaluate(ctx, scope, nil, call.Signature, call.Args...)
+	if err != nil {
+		return err
 	}
-
-	// Evaluate args second.
-	args := cg.Evaluate(ctx, scope, call, b)
-	for i, arg := range call.Arguments() {
+	for i, arg := range call.Args {
 		ctx = WithArg(ctx, i, arg)
 	}
 
-	// Yield before executing the next call statement.
-	ctx = WithFrame(ctx, Frame{call.Name})
-	if cg.dbgr != nil {
-		opt, err := opts.Value().Option()
+	var opts Option
+	if call.WithClause != nil {
+		// Provide a type hint to avoid ambgiuous lookups.
+		kinds := []ast.Kind{
+			ast.Kind(fmt.Sprintf("%s::%s", ast.Option, call.Name)),
+		}
+
+		// If with clause is a call expr, still wrap the scope as if it was a single
+		// element option block.
+		escope := scope
+		if call.WithClause.Expr.CallExpr != nil {
+			escope = ast.NewScope(scope, ast.BlockScope, call.WithClause.Expr.CallExpr)
+		}
+
+		// WithClause provides option expressions access to the binding.
+		values, err := cg.Evaluate(ctx, escope, b, kinds, call.WithClause.Expr)
 		if err != nil {
 			return err
 		}
 
-		err = cg.dbgr.yield(ctx, scope, call, ret.Value(), opt, nil)
+		opts, err = values[0].Option()
+		if err != nil {
+			return err
+		}
+	}
+
+	ctx = WithFrame(ctx, Frame{call.Name})
+
+	// Yield before executing the next call statement.
+	if cg.dbgr != nil {
+		err := cg.dbgr.yield(ctx, scope, call, ret.Value(), opts, nil)
 		if err != nil {
 			return err
 		}
@@ -669,26 +644,21 @@ func (cg *CodeGen) EmitCallStmt(ctx context.Context, scope *ast.Scope, call *ast
 	return cg.EmitIdentExpr(ctx, scope, call.Name, call.Name.Ident, args, opts, binding, ret)
 }
 
-func (cg *CodeGen) Evaluate(ctx context.Context, scope *ast.Scope, call ast.CallNode, b *ast.Binding) []Register {
-	var rets []Register
-	for i, arg := range call.Arguments() {
-		i, arg := i, arg
-		ret := NewRegister(ctx)
-		ret.SetAsync(func(_ Value) (Value, error) {
-			err := cg.lookupCall(ctx, scope, call.Ident())
-			if err != nil {
-				return nil, err
-			}
-
-			ctx := WithProgramCounter(ctx, arg)
-			ctx = WithReturnType(ctx, call.Signature()[i])
-
-			ret := NewRegister(ctx)
-			err = cg.EmitExpr(ctx, scope, arg, nil, b, ret)
-			return ret.Value(), err
-		})
+func (cg *CodeGen) Evaluate(ctx context.Context, scope *ast.Scope, b *ast.Binding, kinds []ast.Kind, exprs ...*ast.Expr) (values []Value, err error) {
+	if len(kinds) != len(exprs) {
+		return nil, errdefs.WithInternalErrorf(ProgramCounter(ctx), "expected %d kinds but got %d", len(exprs), len(kinds))
+	}
+	for i, expr := range exprs {
+		ctx = WithProgramCounter(ctx, expr)
+		ctx = WithReturnType(ctx, kinds[i])
 
-		rets = append(rets, ret)
+		// Evaluated expressions write to a new return register.
+		ret := NewRegister(ctx)
+		err = cg.EmitExpr(ctx, scope, expr, nil, nil, b, ret)
+		if err != nil {
+			return
+		}
+		values = append(values, ret.Value())
 	}
-	return rets
+	return
 }
diff --git a/codegen/value.go b/codegen/value.go
index bece423..3745718 100644
--- a/codegen/value.go
+++ b/codegen/value.go
@@ -36,19 +36,17 @@ type Option []interface{}
 type Register interface {
 	Value() Value
 	Set(interface{}) error
-	SetAsync(func(Value) (Value, error))
+	SetAsync(func(Value) (Value, error)) error
 }
 
 type register struct {
-	debug bool
 	value Value
-	last  Value
+	last  chan Value
 	ctor  func(iface interface{}) (Value, error)
 }
 
 func NewRegister(ctx context.Context) Register {
 	return &register{
-		debug: GetDebugger(ctx) != nil,
 		value: ZeroValue(ctx),
 		ctor: func(iface interface{}) (Value, error) {
 			return NewValue(ctx, iface)
@@ -65,39 +63,37 @@ func (r *register) Set(iface interface{}) error {
 		}
 		return err
 	}
-	r.SetAsync(func(Value) (Value, error) {
+	return r.SetAsync(func(Value) (Value, error) {
 		return val, err
 	})
-	return nil
 }
 
-func (r *register) SetAsync(f func(Value) (Value, error)) {
-	var prev Value
+func (r *register) SetAsync(f func(Value) (Value, error)) error {
+	var prev chan Value
 	if r.last == nil {
-		prev = r.value
+		prev = make(chan Value, 1)
+		prev <- r.value
 	} else {
 		prev = r.last
 	}
 
-	valCh := make(chan Value)
-	r.last = &lazyValue{valCh: valCh}
+	ret := make(chan Value)
+	r.last = ret
 
 	go func() {
-		next, err := f(prev)
+		next, err := f(<-prev)
 		if err != nil {
 			next = &errorValue{err}
 		}
-		valCh <- next
+		ret <- next
 	}()
 
-	if r.debug {
-		r.last.(*lazyValue).wait()
-	}
+	return nil
 }
 
 func (r *register) Value() Value {
 	if r.last != nil {
-		r.value = r.last
+		r.value = <-r.last
 		r.last = nil
 	}
 	return r.value
@@ -212,53 +208,6 @@ func (v *errorValue) Reflect(t reflect.Type) (reflect.Value, error) {
 	return reflect.Value{}, v.err
 }
 
-type lazyValue struct {
-	valCh chan Value
-	val   Value
-}
-
-func (v *lazyValue) wait() {
-	val, ok := <-v.valCh
-	if ok {
-		v.val = val
-		close(v.valCh)
-	}
-}
-
-func (v *lazyValue) Kind() ast.Kind {
-	v.wait()
-	return v.val.Kind()
-}
-
-func (v *lazyValue) Filesystem() (Filesystem, error) {
-	v.wait()
-	return v.val.Filesystem()
-}
-
-func (v *lazyValue) Int() (int, error) {
-	v.wait()
-	return v.val.Int()
-}
-
-func (v *lazyValue) String() (string, error) {
-	v.wait()
-	return v.val.String()
-}
-
-func (v *lazyValue) Option() (Option, error) {
-	v.wait()
-	return v.val.Option()
-}
-
-func (v *lazyValue) Request() (solver.Request, error) {
-	v.wait()
-	return v.val.Request()
-}
-
-func (v *lazyValue) Reflect(t reflect.Type) (reflect.Value, error) {
-	return ReflectTo(v, t)
-}
-
 type zeroValue struct {
 	defaultPlatform specs.Platform
 }
@@ -445,7 +394,6 @@ func (v *reqValue) Reflect(t reflect.Type) (reflect.Value, error) {
 }
 
 var (
-	rValue      = reflect.TypeOf((*Value)(nil)).Elem()
 	rFilesystem = reflect.TypeOf(Filesystem{})
 	rString     = reflect.TypeOf("")
 	rInt        = reflect.TypeOf(0)
@@ -465,8 +413,6 @@ func ReflectTo(v Value, t reflect.Type) (reflect.Value, error) {
 	)
 
 	switch t {
-	case rValue:
-		iface = v
 	case rFilesystem:
 		iface, err = v.Filesystem()
 	case rString:
diff --git a/docs/reference.md b/docs/reference.md
index 686dc7c..87f04eb 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -159,10 +159,17 @@
 
 	#!hlb
 	fs default() {
-		dockerPush "ref"
+		dockerPush "ref" with option {
+			stargz
+		}
 	}
 
 
+#### <span class='hlb-type'>option::dockerPush</span> <span class='hlb-name'>stargz</span>()
+
+
+
+
 
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>download</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>)
 
diff --git a/langserver/server.go b/langserver/server.go
index 9c81ecd..14ad1c9 100644
--- a/langserver/server.go
+++ b/langserver/server.go
@@ -361,7 +361,7 @@ func highlightExpr(lines map[int]lsp.SemanticHighlightingTokens, expr *ast.Expr)
 		if call.Name != nil {
 			highlightIdentExpr(lines, call.Name)
 		}
-		for _, arg := range call.Arguments() {
+		for _, arg := range call.Args() {
 			highlightExpr(lines, arg)
 		}
 	}
diff --git a/language/builtin.hlb b/language/builtin.hlb
index 712b678..fcb6b7b 100644
--- a/language/builtin.hlb
+++ b/language/builtin.hlb
@@ -504,6 +504,16 @@ fs diff(fs base)
 # @return an option to push the filesystem to a registry.
 fs dockerPush(string ref) binds (string digest)
 
+# Compress the image as a eStargz image before pushing.
+#
+# eStargz is a lazily pullable image format. It is compatible to OCI/Docker
+# images so it can be pushed to standard container registries and runnable on
+# eStargz-agnostic runtimes like Docker.
+# See: https://github.com/containerd/stargz-snapshotter
+#
+# @return an option to compress image as eStargz before pushing.
+option::dockerPush stargz()
+
 # Loads the filesystem as a Docker image to the docker client found in your
 # environment.
 #
diff --git a/parser/ast/ast.go b/parser/ast/ast.go
index a36a36c..2763a97 100644
--- a/parser/ast/ast.go
+++ b/parser/ast/ast.go
@@ -108,13 +108,6 @@ type StopNode interface {
 	Subject() Node
 }
 
-type CallNode interface {
-	Node
-	Ident() *Ident
-	Signature() []Kind
-	Arguments() []*Expr
-}
-
 type Mixin struct {
 	Pos    lexer.Position
 	EndPos lexer.Position
@@ -456,7 +449,7 @@ type Stmt struct {
 type CallStmt struct {
 	Mixin
 	Doc        *CommentGroup
-	Sig        []Kind
+	Signature  []Kind
 	Name       *IdentExpr  `parser:"@@"`
 	Args       []*Expr     `parser:"@@*"`
 	WithClause *WithClause `parser:"@@?"`
@@ -475,30 +468,15 @@ func NewCallStmt(name string, args []*Expr, with *WithClause, binds *BindClause)
 	}
 }
 
-func (cs *CallStmt) Breakpoint() bool {
-	if cs.Name == nil || cs.Name.Ident == nil {
-		return false
-	}
-	return cs.Name.Ident.Text == "breakpoint"
-}
-
 func (cs *CallStmt) Subject() Node {
 	return cs.Name
 }
 
-func (cs *CallStmt) Ident() *Ident {
-	if cs.Name == nil {
-		return nil
+func (cs *CallStmt) Breakpoint() bool {
+	if cs.Name == nil || cs.Name.Ident == nil {
+		return false
 	}
-	return cs.Name.Ident
-}
-
-func (cs *CallStmt) Signature() []Kind {
-	return cs.Sig
-}
-
-func (cs *CallStmt) Arguments() []*Expr {
-	return cs.Args
+	return cs.Name.Ident.Text == "breakpoint"
 }
 
 // WithClause represents optional arguments for a CallStmt.
@@ -859,34 +837,23 @@ func NewBoolExpr(v bool) *Expr {
 // expression.
 type CallExpr struct {
 	Mixin
-	Sig  []Kind
-	Name *IdentExpr `parser:"@@"`
-	List *ExprList  `parser:"@@?"`
-}
-
-func (ce *CallExpr) Breakpoint() bool {
-	if ce.Name == nil || ce.Name.Ident == nil {
-		return false
-	}
-	return ce.Name.Ident.Text == "breakpoint"
+	Signature []Kind
+	Name      *IdentExpr `parser:"@@"`
+	List      *ExprList  `parser:"@@?"`
 }
 
 func (ce *CallExpr) Subject() Node {
 	return ce.Name
 }
 
-func (ce *CallExpr) Ident() *Ident {
-	if ce.Name == nil {
-		return nil
+func (ce *CallExpr) Breakpoint() bool {
+	if ce.Name == nil || ce.Name.Ident == nil {
+		return false
 	}
-	return ce.Name.Ident
-}
-
-func (ce *CallExpr) Signature() []Kind {
-	return ce.Sig
+	return ce.Name.Ident.Text == "breakpoint"
 }
 
-func (ce *CallExpr) Arguments() []*Expr {
+func (ce *CallExpr) Args() []*Expr {
 	var args []*Expr
 	if ce.List != nil {
 		for _, field := range ce.List.Fields {
diff --git a/pkg/stargzutil/fixtures/alpine.json b/pkg/stargzutil/fixtures/alpine.json
new file mode 100644
index 0000000..e763e86
--- /dev/null
+++ b/pkg/stargzutil/fixtures/alpine.json
@@ -0,0 +1,16 @@
+{
+  "schemaVersion": 2,
+  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+  "config": {
+    "mediaType": "application/vnd.docker.container.image.v1+json",
+    "size": 1471,
+    "digest": "sha256:c059bfaa849c4d8e4aecaeb3a10c2d9b3d85f5165c66ad3a4d937758128c4d18"
+  },
+  "layers": [
+    {
+      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+      "size": 2818413,
+      "digest": "sha256:59bf1c3509f33515622619af21ed55bbe26d24913cedbca106468a5fb37a50c3"
+    }
+  ]
+}
diff --git a/pkg/stargzutil/fixtures/alpine_desc.json b/pkg/stargzutil/fixtures/alpine_desc.json
new file mode 100644
index 0000000..7fe810a
--- /dev/null
+++ b/pkg/stargzutil/fixtures/alpine_desc.json
@@ -0,0 +1,5 @@
+{
+  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+  "digest": "sha256:e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3",
+  "size": 528
+}
diff --git a/pkg/stargzutil/fixtures/alpine_index.json b/pkg/stargzutil/fixtures/alpine_index.json
new file mode 100644
index 0000000..919a17c
--- /dev/null
+++ b/pkg/stargzutil/fixtures/alpine_index.json
@@ -0,0 +1,72 @@
+{
+  "manifests": [
+    {
+      "digest": "sha256:e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "amd64",
+        "os": "linux"
+      },
+      "size": 528
+    },
+    {
+      "digest": "sha256:e047bc2af17934d38c5a7fa9f46d443f1de3a7675546402592ef805cfa929f9d",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "arm",
+        "os": "linux",
+        "variant": "v6"
+      },
+      "size": 528
+    },
+    {
+      "digest": "sha256:8483ecd016885d8dba70426fda133c30466f661bb041490d525658f1aac73822",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "arm",
+        "os": "linux",
+        "variant": "v7"
+      },
+      "size": 528
+    },
+    {
+      "digest": "sha256:c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "arm64",
+        "os": "linux",
+        "variant": "v8"
+      },
+      "size": 528
+    },
+    {
+      "digest": "sha256:2689e157117d2da668ad4699549e55eba1ceb79cb7862368b30919f0488213f4",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "386",
+        "os": "linux"
+      },
+      "size": 528
+    },
+    {
+      "digest": "sha256:2042a492bcdd847a01cd7f119cd48caa180da696ed2aedd085001a78664407d6",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "ppc64le",
+        "os": "linux"
+      },
+      "size": 528
+    },
+    {
+      "digest": "sha256:49e322ab6690e73a4909f787bcbdb873631264ff4a108cddfd9f9c249ba1d58e",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "s390x",
+        "os": "linux"
+      },
+      "size": 528
+    }
+  ],
+  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
+  "schemaVersion": 2
+}
diff --git a/pkg/stargzutil/fixtures/alpine_index_desc.json b/pkg/stargzutil/fixtures/alpine_index_desc.json
new file mode 100644
index 0000000..437aa8a
--- /dev/null
+++ b/pkg/stargzutil/fixtures/alpine_index_desc.json
@@ -0,0 +1,5 @@
+{
+  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
+  "digest": "sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300",
+  "size": 1638
+}
diff --git a/pkg/stargzutil/fixtures/alpine_stargz.json b/pkg/stargzutil/fixtures/alpine_stargz.json
new file mode 100644
index 0000000..db50355
--- /dev/null
+++ b/pkg/stargzutil/fixtures/alpine_stargz.json
@@ -0,0 +1,20 @@
+{
+  "mediaType": "application/vnd.oci.image.manifest.v1+json",
+  "schemaVersion": 2,
+  "config": {
+    "mediaType": "application/vnd.oci.image.config.v1+json",
+    "digest": "sha256:b200a018d67c97778fbfc47175bc86c30f8040f583eb1809bbfc7c05d34aa778",
+    "size": 703
+  },
+  "layers": [
+    {
+      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+      "digest": "sha256:8ba6cec101df53fb9ce1031ceaa2c701b7c5b250cefad8c74806ff02406cb14a",
+      "size": 2833104,
+      "annotations": {
+        "containerd.io/snapshot/stargz/toc.digest": "sha256:77dd89754ec6fda86dacacca3b1b1d2beac3f626177819d09ece10efcd4dbe3a",
+        "io.containers.estargz.uncompressed-size": "5966336"
+      }
+    }
+  ]
+}
diff --git a/pkg/stargzutil/fixtures/alpine_stargz_desc.json b/pkg/stargzutil/fixtures/alpine_stargz_desc.json
new file mode 100644
index 0000000..bed80f2
--- /dev/null
+++ b/pkg/stargzutil/fixtures/alpine_stargz_desc.json
@@ -0,0 +1,5 @@
+{
+  "mediaType": "application/vnd.oci.image.manifest.v1+json",
+  "digest": "sha256:4382407e6f4fab29345722ba819c33f9d158b1bce240839e889d3ff715f0ad93",
+  "size": 739
+}
diff --git a/pkg/stargzutil/stargzutil.go b/pkg/stargzutil/stargzutil.go
new file mode 100644
index 0000000..df1ee36
--- /dev/null
+++ b/pkg/stargzutil/stargzutil.go
@@ -0,0 +1,81 @@
+package stargzutil
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/containerd/containerd/images"
+	"github.com/containerd/containerd/platforms"
+	"github.com/containerd/containerd/remotes"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// HasNonStargzLayer traverses a manifest by resolving the reference and
+// walking over its children to see if there's any non-stargz layers.
+//
+// If ref points to a manifest list, the platform matcher is used to only
+// consider the current platform. Although BuildKit supports building manifest
+// lists, and multi-platform conversion is technically possible client side,
+// it requires pulling blobs locally (not via BuildKit) which is undesirable.
+//
+// See: https://github.com/containerd/stargz-snapshotter/blob/v0.6.4/nativeconverter/estargz/estargz.go
+func HasNonStargzLayer(ctx context.Context, resolver remotes.Resolver, matcher platforms.MatchComparer, ref string) (bool, error) {
+	_, desc, err := resolver.Resolve(ctx, ref)
+	if err != nil {
+		return false, err
+	}
+
+	fetcher, err := resolver.Fetcher(ctx, ref)
+	if err != nil {
+		return false, err
+	}
+
+	nonStargz := false
+	err = images.Walk(ctx, images.HandlerFunc(func(ctx context.Context, desc specs.Descriptor) ([]specs.Descriptor, error) {
+		switch desc.MediaType {
+		case images.MediaTypeDockerSchema2Manifest, specs.MediaTypeImageManifest:
+			rc, err := fetcher.Fetch(ctx, desc)
+			if err != nil {
+				return nil, err
+			}
+			defer rc.Close()
+
+			var mfst specs.Manifest
+			err = json.NewDecoder(rc).Decode(&mfst)
+			if err != nil {
+				return nil, err
+			}
+
+			for _, layer := range mfst.Layers {
+				_, ok := layer.Annotations["containerd.io/snapshot/stargz/toc.digest"]
+				if !ok {
+					nonStargz = true
+					break
+				}
+			}
+			return nil, nil
+		case images.MediaTypeDockerSchema2ManifestList, specs.MediaTypeImageIndex:
+			rc, err := fetcher.Fetch(ctx, desc)
+			if err != nil {
+				return nil, err
+			}
+			defer rc.Close()
+
+			var idx specs.Index
+			err = json.NewDecoder(rc).Decode(&idx)
+			if err != nil {
+				return nil, err
+			}
+
+			for _, d := range idx.Manifests {
+				if d.Platform == nil || matcher.Match(*d.Platform) {
+					return []specs.Descriptor{d}, nil
+				}
+			}
+			return nil, fmt.Errorf("failed to find manifest matching platform")
+		}
+		return nil, fmt.Errorf("unexpected media type %v for %v", desc.MediaType, desc.Digest)
+	}), desc)
+	return nonStargz, err
+}
diff --git a/solver/solve.go b/solver/solve.go
index 33aa68a..606086e 100644
--- a/solver/solve.go
+++ b/solver/solve.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 
 	"github.com/docker/buildx/util/progress"
+	"github.com/docker/distribution/reference"
 	"github.com/moby/buildkit/client"
 	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/exporter/containerimage/exptypes"
@@ -20,17 +21,19 @@ type SolveOption func(*SolveInfo) error
 type SolveCallback func(ctx context.Context, resp *client.SolveResponse) error
 
 type SolveInfo struct {
-	Evaluate              bool
-	OutputMoby            bool
-	OutputDockerRef       string
-	OutputPushImage       string
-	OutputLocal           string
-	OutputLocalTarball    bool
-	OutputLocalOCITarball bool
-	Callbacks             []SolveCallback `json:"-"`
-	ImageSpec             *ImageSpec
-	ErrorHandler          ErrorHandler
-	Entitlements          []entitlements.Entitlement
+	Evaluate               bool
+	OutputMoby             bool
+	OutputDockerRef        string
+	OutputPushImage        string
+	OutputLocal            string
+	OutputLocalTarball     bool
+	OutputLocalOCITarball  bool
+	OutputStargz           bool
+	OutputForceCompression bool
+	Callbacks              []SolveCallback `json:"-"`
+	ImageSpec              *ImageSpec
+	ErrorHandler           ErrorHandler
+	Entitlements           []entitlements.Entitlement
 }
 
 // ImageSpec is HLB's wrapper for the OCI specs image, allowing for backward
@@ -39,6 +42,10 @@ type ImageSpec struct {
 	specs.Image
 
 	ContainerConfig ContainerConfig `json:"container_config,omitempty"`
+
+	// Canonical is the fully qualified reference of the image with name and
+	// digest.
+	Canonical reference.Canonical `json:"-"`
 }
 
 // ContainerConfig is the schema1-compatible configuration of the container
@@ -134,6 +141,14 @@ func WithErrorHandler(errorHandler ErrorHandler) SolveOption {
 	}
 }
 
+func WithStargz(forceCompression bool) SolveOption {
+	return func(info *SolveInfo) error {
+		info.OutputStargz = true
+		info.OutputForceCompression = forceCompression
+		return nil
+	}
+}
+
 func Solve(ctx context.Context, c *client.Client, s *session.Session, pw progress.Writer, def *llb.Definition, opts ...SolveOption) error {
 	info := &SolveInfo{}
 	for _, opt := range opts {
@@ -206,6 +221,13 @@ func Build(ctx context.Context, c *client.Client, s *session.Session, pw progres
 		if info.OutputMoby {
 			entry.Type = "moby"
 		}
+		if info.OutputStargz {
+			entry.Attrs["compression"] = "estargz"
+			entry.Attrs["oci-mediatypes"] = "true"
+		}
+		if info.OutputForceCompression {
+			entry.Attrs["force-compression"] = "true"
+		}
 		solveOpt.Exports = append(solveOpt.Exports, entry)
 	}
 


Test patch:
diff --git a/pkg/stargzutil/stargzutil_test.go b/pkg/stargzutil/stargzutil_test.go
new file mode 100644
index 0000000..6a0ae1b
--- /dev/null
+++ b/pkg/stargzutil/stargzutil_test.go
@@ -0,0 +1,110 @@
+package stargzutil
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"testing"
+
+	_ "embed"
+
+	"github.com/containerd/containerd/platforms"
+	"github.com/containerd/containerd/remotes"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
+	"github.com/stretchr/testify/require"
+)
+
+var (
+	//go:embed fixtures/alpine_desc.json
+	alpineDesc []byte
+
+	//go:embed fixtures/alpine.json
+	alpine []byte
+
+	//go:embed fixtures/alpine_index_desc.json
+	alpineIndexDesc []byte
+
+	//go:embed fixtures/alpine_index.json
+	alpineIndex []byte
+
+	//go:embed fixtures/alpine_stargz_desc.json
+	alpineStargzDesc []byte
+
+	//go:embed fixtures/alpine_stargz.json
+	alpineStargz []byte
+)
+
+type testResolver struct{}
+
+func (ts *testResolver) Resolve(ctx context.Context, ref string) (name string, desc specs.Descriptor, err error) {
+	var dt []byte
+	switch ref {
+	case "alpine":
+		dt = alpineDesc
+	case "alpine:multiplatform":
+		dt = alpineIndexDesc
+	case "alpine:stargz":
+		dt = alpineStargzDesc
+	default:
+		err = fmt.Errorf("unrecognized ref %s", ref)
+		return
+	}
+	return ref, desc, json.Unmarshal(dt, &desc)
+}
+
+func (ts *testResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
+	return &testFetcher{}, nil
+}
+
+func (ts *testResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
+	return nil, fmt.Errorf("unimplemented")
+}
+
+type testFetcher struct{}
+
+func (tf *testFetcher) Fetch(ctx context.Context, d specs.Descriptor) (io.ReadCloser, error) {
+	var dt []byte
+	switch d.Digest.String() {
+	// alpine.json
+	case "sha256:e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3":
+		dt = alpine
+	// alpine_index.json
+	case "sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300":
+		dt = alpineIndex
+	// alpine_stargz.json
+	case "sha256:4382407e6f4fab29345722ba819c33f9d158b1bce240839e889d3ff715f0ad93":
+		dt = alpineStargz
+	default:
+		return nil, fmt.Errorf("unrecognized digest %s", d.Digest)
+	}
+	return io.NopCloser(bytes.NewReader(dt)), nil
+}
+
+func TestHasNonStargzLayer(t *testing.T) {
+	ctx := context.Background()
+	resolver := &testResolver{}
+	platform := specs.Platform{OS: "linux", Architecture: "amd64"}
+
+	type testCase struct {
+		ref      string
+		expected bool
+	}
+
+	for _, tc := range []testCase{{
+		"alpine", true,
+	}, {
+		"alpine:multiplatform", true,
+	}, {
+		"alpine:stargz", false,
+	}} {
+		tc := tc
+		t.Run(tc.ref, func(t *testing.T) {
+			t.Parallel()
+			actual, err := HasNonStargzLayer(ctx, resolver, platforms.Only(platform), tc.ref)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, actual)
+		})
+	}
+}