Repository: openllb/hlb
Pull request #141 (2020-05-08 23:56:58)

Allow local to specify a single file instead of a directory
- Currently, if you specify a single file like `local "manifest.json"` then it fails with:
```sh
ERROR   failed to solve: rpc error: code = Unknown desc = error from sender: manifest.json is not a directory
```
- Instead `local "manifest.json"` should be the equivalent of: `local "." with option { includePatterns "manifest.json"; }`, with the caveat that if the `path` is for a file then include and exclude patterns are ignored.


Patch:
diff --git a/codegen/chain.go b/codegen/chain.go
index f47193c..8cadf78 100644
--- a/codegen/chain.go
+++ b/codegen/chain.go
@@ -232,12 +232,26 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 			return fc, err
 		}
 
+		fi, err := os.Stat(path)
+		if err != nil {
+			return fc, err
+		}
+
 		var opts []llb.LocalOption
 		for _, iopt := range iopts {
 			opt := iopt.(llb.LocalOption)
 			opts = append(opts, opt)
 		}
 
+		if !fi.IsDir() {
+			filename := filepath.Base(path)
+			path = filepath.Dir(path)
+
+			// When path is a filename instead of a directory, include and exclude
+			// patterns should be ignored.
+			opts = append(opts, llb.IncludePatterns([]string{filename}), llb.ExcludePatterns([]string{}))
+		}
+
 		id, err := cg.LocalID(ctx, path, opts...)
 		if err != nil {
 			return fc, err
diff --git a/docs/reference.md b/docs/reference.md
index 2ffdb2b..bf96d47 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -328,10 +328,10 @@ and entrypoint.
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>local</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
-	the local path to the directory to sync up.
+	the local path to a file or directory to sync up.
 
-A filesystem with the files synced up from a directory on the
-local system.
+A filesystem with the files synced up from a file or directory on the local
+system.
 
 	#!hlb
 	fs default() {
@@ -348,7 +348,8 @@ local system.
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>pattern</span>"
 	a list of patterns for files that should not be synced.
 
-Sync only files that do not match any of the excluded patterns.
+Sync only files that do not match any of the excluded patterns. If local
+path is for a file, then exclude patterns are ignored.
 
 #### <span class='hlb-type'>option::local</span> <span class='hlb-name'>followPaths</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
@@ -362,7 +363,8 @@ Sync the targets of symlinks if path is to a symlink.
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>pattern</span>"
 	a list of patterns for files that should be synced.
 
-Sync only files that match any of the included patterns.
+Sync only files that match any of the included patterns. If local path is
+for a file, then include patterns are ignored.
 
 
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>mkdir</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>, <span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>)
diff --git a/language/builtin.hlb b/language/builtin.hlb
index e05b66a..035fa7c 100644
--- a/language/builtin.hlb
+++ b/language/builtin.hlb
@@ -54,20 +54,22 @@ fs git(string remote, string ref)
 # @return the option to keep the `.git` directory.
 option::git keepGitDir()
 
-# A filesystem with the files synced up from a directory on the
-# local system.
+# A filesystem with the files synced up from a file or directory on the local
+# system.
 #
-# @param path the local path to the directory to sync up.
+# @param path the local path to a file or directory to sync up.
 # @return a filesystem containing local files.
 fs local(string path)
 
-# Sync only files that match any of the included patterns.
+# Sync only files that match any of the included patterns. If local path is
+# for a file, then include patterns are ignored.
 #
 # @param pattern a list of patterns for files that should be synced.
 # @return an option to sync files that match any pattern.
 option::local includePatterns(variadic string pattern)
 
-# Sync only files that do not match any of the excluded patterns.
+# Sync only files that do not match any of the excluded patterns. If local
+# path is for a file, then exclude patterns are ignored.
 #
 # @param pattern a list of patterns for files that should not be synced.
 # @return an option to sync files that don't match any pattern.


Test patch:
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 06e8286..6809974 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -29,6 +29,18 @@ func Expect(t *testing.T, st llb.State, opts ...solver.SolveOption) solver.Reque
 	})
 }
 
+func (cg *CodeGen) Local(t *testing.T, path string, opts ...llb.LocalOption) llb.State {
+	id, err := cg.LocalID(context.Background(), ".", opts...)
+	require.NoError(t, err)
+
+	opts = append([]llb.LocalOption{
+		llb.SessionID(cg.SessionID()),
+		llb.SharedKeyHint(path),
+	}, opts...)
+
+	return llb.Local(id, opts...)
+}
+
 type testCase struct {
 	name    string
 	targets []string
@@ -42,7 +54,6 @@ func cleanup(value string) string {
 
 func TestCodeGen(t *testing.T) {
 	t.Parallel()
-	ctx := context.Background()
 	for _, tc := range []testCase{{
 		"image",
 		[]string{"default"},
@@ -343,10 +354,36 @@ func TestCodeGen(t *testing.T) {
 		}
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
-			id, err := cg.LocalID(ctx, ".")
-			require.NoError(t, err)
-
-			return Expect(t, llb.Local(id, llb.SessionID(cg.SessionID()), llb.SharedKeyHint(".")))
+			return Expect(t, cg.Local(t, "."))
+		},
+	}, {
+		"local file",
+		[]string{"default"},
+		`
+		fs default() {
+			local "codegen_test.go"
+		}
+		`,
+		func(t *testing.T, cg *CodeGen) solver.Request {
+			return Expect(t, cg.Local(t, ".",
+				llb.IncludePatterns([]string{"codegen_test.go"}),
+			))
+		},
+	}, {
+		"local file with patterns",
+		[]string{"default"},
+		`
+		fs default() {
+			local "codegen_test.go" with option {
+				includePatterns "ignored"
+				excludePatterns "ignored"
+			}
+		}
+		`,
+		func(t *testing.T, cg *CodeGen) solver.Request {
+			return Expect(t, cg.Local(t, ".",
+				llb.IncludePatterns([]string{"codegen_test.go"}),
+			))
 		},
 	}, {
 		"local env",
@@ -603,19 +640,15 @@ func TestCodeGen(t *testing.T) {
 		}
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
-			id, err := cg.LocalID(ctx, ".")
-			require.NoError(t, err)
 			sid := SecretID("codegen_test.go")
-
 			return Expect(t, llb.Image("busybox").Run(
 				llb.Shlex("find ."),
 				llb.Dir("/foo"),
 				llb.AddMount(
 					"/foo",
-					llb.Local(
-						id,
-						llb.SessionID(cg.SessionID()),
-						llb.SharedKeyHint("."),
+					cg.Local(
+						t,
+						".",
 					).File(
 						// this Mkdir is made implicitly due to /foo/secret
 						// secret over readonly FS