Repository: moby/buildkit

Issue:
shebang not respected using heredoc with tabs stripped
Looks like shebang is not respected on `RUN` command if you have a heredoc with tabs stripped

If I have (heredoc without tab strip):

**Dockerfile.ok**

```Dockerfile
# syntax=docker/dockerfile:1.4
FROM bash
RUN <<EOT
#!/usr/bin/env bash
ps | grep $$
EOT
```

I can run:

```Shell
docker buildx build -f Dockerfile.ok --no-cache .
```

And I get something like:
```Shell
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile.ok
#1 transferring dockerfile: 128B done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s

#3 resolve image config for docker.io/docker/dockerfile:1.4
#3 DONE 0.5s

#4 docker-image://docker.io/docker/dockerfile:1.4@sha256:9ba7531bd80fb0a858632727cf7a112fbfd19b17e94c4e84ced81e24ef1a0dbc
#4 CACHED

#5 [internal] load metadata for docker.io/library/bash:latest
#5 DONE 0.5s

#6 [1/2] FROM docker.io/library/bash@sha256:ce062497c248eb1cf4d32927f8c1780cce158d3ed0658c586a5be7308d583cbb
#6 CACHED

#7 [internal] preparing inline document
#7 CACHED

#8 [2/2] RUN <<EOT (#!/usr/bin/env bash...)
#8 0.151     1 root      0:00 bash /dev/pipes/EOT
#8 0.151     8 root      0:00 grep 1
#8 DONE 0.2s

#9 exporting to image
#9 exporting layers 0.1s done
#9 writing image sha256:fef2554f3b9106230c8fe1796087e4ada1c63fb3cf516eb4680678de5e816794 done
#9 DONE 0.1s
```

Where you can see in `1 root      0:00 bash /dev/pipes/EOT` output that the shell used is `bash`.

On the other hand, If I have (heredoc with tab strip):

**Dockerfile.ko**

```Dockerfile
# syntax=docker/dockerfile:1.4
FROM bash
RUN <<-EOT
	#!/usr/bin/env bash
	ps | grep $$
EOT
```

I can run:

```Shell
docker buildx build -f Dockerfile.ko --no-cache .
```

And I get something like:
```Shell
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile.ko
#1 transferring dockerfile: 228B done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s

#3 [auth] docker/dockerfile:pull token for registry-1.docker.io
#3 DONE 0.0s

#4 resolve image config for docker.io/docker/dockerfile:1.4
#4 DONE 1.1s

#5 docker-image://docker.io/docker/dockerfile:1.4@sha256:9ba7531bd80fb0a858632727cf7a112fbfd19b17e94c4e84ced81e24ef1a0dbc
#5 CACHED

#6 [auth] library/bash:pull token for registry-1.docker.io
#6 DONE 0.0s

#7 [internal] load metadata for docker.io/library/bash:latest
#7 DONE 1.0s

#8 [1/2] FROM docker.io/library/bash@sha256:ce062497c248eb1cf4d32927f8c1780cce158d3ed0658c586a5be7308d583cbb
#8 CACHED

#9 [2/2] RUN <<-EOT (#!/usr/bin/env bash...)
#9 0.146     1 root      0:00 /bin/sh -c #!/usr/bin/env bash ps | grep $$ 
#9 0.146     8 root      0:00 grep 1
#9 DONE 0.2s

#10 exporting to image
#10 exporting layers 0.1s done
#10 writing image sha256:3131df0782a800571aced5d594ed84c64b53ba3a7de69649c78d650d20128236 done
#10 DONE 0.1s
```

Where you can see in `1 root      0:00 /bin/sh -c #!/usr/bin/env bash ps | grep $$ ` output that the shell used is `/bin/sh` (not bash, as specified in the shebang `#!/usr/bin/env bash`).

As a side note, I also checked that the heredoc tab strip is really done so it looks like the issue is that the shebang is checked before the heredoc tab strip is done.

Hints from issue comments:
@tonistiigi: @jedevc 

Solution patch:
diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go
index fed1177c6..77354ec15 100644
--- a/frontend/dockerfile/dockerfile2llb/convert.go
+++ b/frontend/dockerfile/dockerfile2llb/convert.go
@@ -1243,7 +1243,12 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
 		}
 
 		if heredoc := parser.MustParseHeredoc(args[0]); heredoc != nil {
-			if d.image.OS != "windows" && strings.HasPrefix(c.Files[0].Data, "#!") {
+			data := c.Files[0].Data
+			if c.Files[0].Chomp {
+				data = parser.ChompHeredocContent(data)
+			}
+
+			if d.image.OS != "windows" && strings.HasPrefix(data, "#!") {
 				// This is a single heredoc with a shebang, so create a file
 				// and run it.
 				// NOTE: choosing to expand doesn't really make sense here, so
@@ -1252,10 +1257,6 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
 				destPath := "/dev/pipes/"
 
 				f := c.Files[0].Name
-				data := c.Files[0].Data
-				if c.Files[0].Chomp {
-					data = parser.ChompHeredocContent(data)
-				}
 				st := llb.Scratch().Dir(sourcePath).File(
 					llb.Mkfile(f, 0755, []byte(data)),
 					dockerui.WithInternalName("preparing inline document"),
@@ -1272,10 +1273,6 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
 				// the syntax can still be used for shells that don't support
 				// heredocs directly.
 				// NOTE: like above, we ignore the expand option.
-				data := c.Files[0].Data
-				if c.Files[0].Chomp {
-					data = parser.ChompHeredocContent(data)
-				}
 				args = []string{data}
 			}
 			customname += fmt.Sprintf(" (%s)", summarizeHeredoc(c.Files[0].Data))



Test patch:
diff --git a/frontend/dockerfile/dockerfile_heredoc_test.go b/frontend/dockerfile/dockerfile_heredoc_test.go
index 9287bd942..04fbd672f 100644
--- a/frontend/dockerfile/dockerfile_heredoc_test.go
+++ b/frontend/dockerfile/dockerfile_heredoc_test.go
@@ -295,16 +295,26 @@ func testRunShebangHeredoc(t *testing.T, sb integration.Sandbox) {
 	dockerfile := []byte(`
 FROM busybox AS build
 
+WORKDIR /dest
+
 RUN <<EOF
 #!/bin/awk -f
 BEGIN {
-	print "hello" >> "/dest"
-	print "world" >> "/dest"
+	print "hello" >> "./out1"
+	print "world" >> "./out1"
 }
 EOF
 
+RUN <<-EOF
+	#!/bin/awk -f
+	BEGIN {
+		print "hello" >> "./out2"
+		print "world" >> "./out2"
+	}
+EOF
+
 FROM scratch
-COPY --from=build /dest /dest
+COPY --from=build /dest /
 `)
 
 	dir := integration.Tmpdir(
@@ -332,9 +342,16 @@ COPY --from=build /dest /dest
 	}, nil)
 	require.NoError(t, err)
 
-	dt, err := os.ReadFile(filepath.Join(destDir, "dest"))
-	require.NoError(t, err)
-	require.Equal(t, "hello\nworld\n", string(dt))
+	contents := map[string]string{
+		"out1": "hello\nworld\n",
+		"out2": "hello\nworld\n",
+	}
+
+	for name, content := range contents {
+		dt, err := os.ReadFile(filepath.Join(destDir, name))
+		require.NoError(t, err)
+		require.Equal(t, content, string(dt))
+	}
 }
 
 func testRunComplexHeredoc(t *testing.T, sb integration.Sandbox) {