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.

Discussion:
@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) {