diff --git a/builtin/lookup.go b/builtin/lookup.go
index 1e712220..dc733b61 100644
--- a/builtin/lookup.go
+++ b/builtin/lookup.go
@@ -26,6 +26,13 @@ var (
Params: []*ast.Field{},
Effects: []*ast.Field{},
},
+ "cache": {
+ Params: []*ast.Field{
+ ast.NewField(ast.Filesystem, "input", false),
+ ast.NewField(ast.String, "ref", false),
+ },
+ Effects: []*ast.Field{},
+ },
"image": {
Params: []*ast.Field{
ast.NewField(ast.String, "ref", false),
@@ -692,6 +699,14 @@ var (
# @return a scratch filesystem.
fs scratch()
+# Caches an input fs by its vertex digest.
+# If the image exists then use that image instead of executing input.
+#
+# @param input a filesystem to cache by vertex digest.
+# @param ref a docker registry reference.
+# @return a filesystem of input from either building or image.
+fs cache(fs input, string ref)
+
# An OCI image's filesystem.
#
# @param ref a docker registry reference. if not fully qualified, it will be
diff --git a/codegen/builtin.go b/codegen/builtin.go
index 46da6a87..f67d4e19 100644
--- a/codegen/builtin.go
+++ b/codegen/builtin.go
@@ -17,6 +17,7 @@ var (
"git": Git{},
"local": Local{},
"frontend": Frontend{},
+ "cache": Cache{},
"run": Run{},
"env": Env{},
"dir": Dir{},
@@ -108,7 +109,7 @@ var (
"readonly": Readonly{},
"tmpfs": Tmpfs{},
"sourcePath": SourcePath{},
- "cache": Cache{},
+ "cache": MountCache{},
},
"option::mkdir": {
"createParents": CreateParents{},
diff --git a/codegen/builtin_fs.go b/codegen/builtin_fs.go
index ce0ea76b..23988779 100644
--- a/codegen/builtin_fs.go
+++ b/codegen/builtin_fs.go
@@ -328,6 +328,74 @@ func (f Frontend) Call(ctx context.Context, cln *client.Client, val Value, opts
return NewValue(ctx, fs)
}
+type Cache struct{}
+
+func (c Cache) Call(ctx context.Context, cln *client.Client, val Value, opts Option, input Filesystem, ref string) (Value, error) {
+ inputDgst, err := input.Digest(ctx)
+ if err != nil {
+ return nil, Arg(ctx, 1).WithError(err)
+ }
+ named, err := reference.ParseNormalizedNamed(ref)
+ if err != nil {
+ return nil, errdefs.WithInvalidImageRef(err, Arg(ctx, 1), ref)
+ }
+ namedTagged, err := reference.WithTag(named, inputDgst.Encoded())
+ if err != nil {
+ return nil, Arg(ctx, 1).WithError(err)
+ }
+ ref = namedTagged.String()
+
+ resolver := ImageResolver(ctx)
+ if resolver != nil {
+ resolveOpt := llb.ResolveImageConfigOpt{
+ Platform: &input.Platform,
+ }
+
+ dgst, config, err := resolver.ResolveImageConfig(ctx, ref, resolveOpt)
+ if err == nil {
+ var imageOpts []llb.ImageOption
+ imageOpts = append(imageOpts, llb.Platform(input.Platform))
+ for _, opt := range SourceMap(ctx) {
+ imageOpts = append(imageOpts, opt)
+ }
+
+ canonical, err := reference.WithDigest(named, dgst)
+ if err != nil {
+ return nil, errdefs.WithInvalidImageRef(err, Arg(ctx, 1), ref)
+ }
+
+ cacheVal, err := NewValue(ctx, llb.Image(canonical.String()))
+ if err != nil {
+ return nil, Arg(ctx, 1).WithError(err)
+ }
+
+ input, err = cacheVal.Filesystem()
+ if err != nil {
+ return nil, Arg(ctx, 1).WithError(err)
+ }
+
+ input.State, err = input.State.WithImageConfig(config)
+ if err != nil {
+ return nil, Arg(ctx, 1).WithError(err)
+ }
+
+ input.Image = &solver.ImageSpec{}
+ err = json.Unmarshal(config, input.Image)
+ if err != nil {
+ return nil, Arg(ctx, 1).WithError(err)
+ }
+ } else { // not found
+ inputVal, err := NewValue(ctx, input)
+ if err != nil {
+ return nil, Arg(ctx, 0).WithError(err)
+ }
+ return (DockerPush{}).Call(ctx, cln, inputVal, opts, ref)
+ }
+ }
+
+ return NewValue(ctx, input)
+}
+
type Env struct{}
func (e Env) Call(ctx context.Context, cln *client.Client, val Value, opts Option, key, value string) (Value, error) {
diff --git a/codegen/builtin_option.go b/codegen/builtin_option.go
index cbfb60b7..12b5485a 100644
--- a/codegen/builtin_option.go
+++ b/codegen/builtin_option.go
@@ -693,10 +693,10 @@ func (m Mount) Call(ctx context.Context, cln *client.Client, val Value, opts Opt
return nil, err
}
- var cache *Cache
+ var cache *MountCache
for _, opt := range opts {
var ok bool
- cache, ok = opt.(*Cache)
+ cache, ok = opt.(*MountCache)
if ok {
break
}
@@ -858,11 +858,11 @@ func (sp SourcePath) Call(ctx context.Context, cln *client.Client, val Value, op
return NewValue(ctx, append(retOpts, llbutil.WithSourcePath(path)))
}
-type Cache struct {
+type MountCache struct {
ast.Node
}
-func (c Cache) Call(ctx context.Context, cln *client.Client, val Value, opts Option, id, mode string) (Value, error) {
+func (mc MountCache) Call(ctx context.Context, cln *client.Client, val Value, opts Option, id, mode string) (Value, error) {
retOpts, err := val.Option()
if err != nil {
return nil, err
@@ -880,7 +880,7 @@ func (c Cache) Call(ctx context.Context, cln *client.Client, val Value, opts Opt
return nil, errdefs.WithInvalidSharingMode(Arg(ctx, 1), mode, []string{"shared", "private", "locked"})
}
- retOpts = append(retOpts, &Cache{ProgramCounter(ctx)}, llbutil.WithPersistentCacheDir(id, sharing))
+ retOpts = append(retOpts, &MountCache{ProgramCounter(ctx)}, llbutil.WithPersistentCacheDir(id, sharing))
return NewValue(ctx, retOpts)
}
diff --git a/docs/reference.md b/docs/reference.md
index 87f04eb7..5305918e 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -1,4 +1,20 @@
## fs functions
+### fs cache(fs input, string ref)
+
+!!! info "fs input"
+
+!!! info "string ref"
+
+
+
+
+ #!hlb
+ fs default() {
+ cache scratch "ref"
+ }
+
+
+
### fs cmd(string args)
!!! info "string args"
diff --git a/language/builtin.hlb b/language/builtin.hlb
index fcb6b7b0..77cb08f7 100644
--- a/language/builtin.hlb
+++ b/language/builtin.hlb
@@ -3,6 +3,14 @@
# @return a scratch filesystem.
fs scratch()
+# Caches an input fs by its vertex digest.
+# If the image exists then use that image instead of executing input.
+#
+# @param input a filesystem to cache by vertex digest.
+# @param ref a docker registry reference.
+# @return a filesystem of input from either building or image.
+fs cache(fs input, string ref)
+
# An OCI image's filesystem.
#
# @param ref a docker registry reference. if not fully qualified, it will be