Repository: docker/buildx

Issue:
docker buildx bake should apply env variable from .env
docker buildx should read .env file like compose. Kudos

docker-compose.yml
```
version: '3.6'

services:
    ssr:
        build:
            context: .
            target: ssr
            args:
                COMMIT_SHA: ${COMMIT_SHA}
```
.env
```
COMMIT_SHA=a06baf1a
```

docker-compose config
```
services:
  ssr:
    build:
      args:
        COMMIT_SHA: a06baf1a
      context: 
      target: ssr
version: '3.6'
```

docker buildx bake --print
```
{
   "target": {
      "ssr": {
         "context": ".",
         "dockerfile": "Dockerfile",
         "args": {
            "COMMIT_SHA": ""
         },
         "target": "ssr"
      }
   }
}
```


Hints from issue comments:
@tonistiigi: @tiborvass @FernandoMiguel wdyt? I remember discussing this case some time ago with at least one of you.@FernandoMiguel: I remember opening this 
https://github.com/docker/buildx/issues/111

but yeah, there is expectation for those values to be read...@ktns: If you think it's a burden to have an docker-compose.yml parser in buildx for this, what about using `docker-compose config` command for expanding environment variables? It outputs the content with all environment variables substituted.

Maybe it should not be a default behavior, but I'll be happy if there is such an option.@crazy-max: If it's the right direction, #489 can be tested if you want: https://github.com/docker/buildx/suites/1696585811/artifacts/31897347@aeaton-overleaf: I'm working around this with `env $(cat .env | grep -v "#" | xargs) docker buildx …` but it would be useful to be able to specify `--env-file=.env` as an option even if it's not on by default.@crazy-max: Fixed by https://github.com/docker/buildx/pull/541@zachary-povey: Hi 👋 - I think this should be re-opened. 

#541 allows specifying this kind of file for HCL configurations but doesn't give any options for people still using `docker-compose`. The `.env` file is still not read automatically and passing it through using `-f` also does not work nor does using the default `docker-bake.override.hcl` file.

@tonistiigi mentions this in the issue:

> The HCL attributes are not 100% compatible with env file format so I think for compose support we still should have a fallback, and maybe still automatically load it if you parse compose. Possibly we can just use the new compose library that does that automatically on parsing compose. For everything else, you can use HCL/JSON attributes.@trinitronx: @crazy-max  I'd tend to agree with @zachary-povey given the caveat of @tonistiigi's observation that an `env.hcl` file is not 100% compatible to `.env` (although in theory, a subset of HCL `key="value"` pairs could suffice to be valid in both contexts).

### Feature Idea

For example, the following commands could be made to work functionally equivalent for most general variable substitution purposes given the same `.env` file:

    # docker-compose.buildx.yml: Could use x-bake extension to docker-compose.yaml syntax
    # with matching definitions in an HCL equivalent file: docker-buildx-bake.hcl
    docker buildx bake --file docker-compose.buildx.yml --file .env --print multi-platform-demo
    docker buildx bake --file docker-buildx-bake.hcl    --file .env --print multi-platform-demo
    docker-compose -f docker-compose.buildx.yml  config

#### Input Files:

`.env`:
```HCL
# vim: syntax=hcl filetype=hcl
# Note: Quoted k="v" syntax supports both docker-compose & docker buildx bake
#       Try to keep this file simple w/o advanced HCL constructs
FOO="bar"
```

`docker-compose.buildx.yml`:
```YAML
# vim: syntax=yaml filetype=yaml
version: "3.4"

services:
  multi-platform-demo:
    build:
      context: .
      # dockerfile: ./Dockerfile
      # args:
      target: release
      x-bake:
        tags:
          - test-tag:${FOO}
        platforms:
          - linux/amd64
          - linux/arm64
          - linux/ppc64le
          - linux/riscv64
          - linux/s390x
          - linux/386
          - linux/arm/v7
          - linux/arm/v6
        cache-from:
          - type=local,src=./cache/multi-platform-demo
        cache-to:
          - type=local,dest=./cache/multi-platform-demo
        output: type=registry,push=false,name=trinitronx/multi-platform-demo
```

```HCL
# vim: syntax=hcl filetype=hcl
variable "FOO" { default = "foo" }

target "multi-platform-demo" {
  context = "."
  # dockerfile = "./Dockerfile"
  # args = {}
  
  target = "release"
  # BuildX (e.g. docker-compose.buildx.yml x-bake) {
    tags = [ "test-tag:${FOO}" ]
    platforms = [
      "linux/amd64",
      "linux/arm64",
      "linux/ppc64le",
      "linux/riscv64",
      "linux/s390x",
      "linux/386",
      "linux/arm/v7",
      "linux/arm/v6"
    ]

    cache-from = [ "type=local,src=./cache/multi-platform-demo" ]
    cache-to = [ "type=local,dest=./cache/multi-platform-demo" ]
    output = [ "type=registry,push=false,name=trinitronx/multi-platform-demo" ]
  # } end x-bake equivalent
}
```

### Expected Result

The first two `buildx` commands could return the same JSON, while the `docker-compose` one performs the same variable substitution given the `.env` file and outputs the usual YAML format.

### Actual Result

```bash
diff -Naur \
  <(docker buildx bake --file docker-compose.buildx.yml --file .env --print multi-platform-demo) \
  <(docker buildx bake --file docker-buildx-bake.hcl    --file .env --print multi-platform-demo)
```
```diff
WARN[0000] The "FOO" variable is not set. Defaulting to a blank string.
[+] Building 0.0s (0/0)
[+] Building 0.0s (0/0)
--- /dev/fd/63	2022-02-18 20:44:16.000000000 -0700
+++ /dev/fd/62	2022-02-18 20:44:16.000000000 -0700
@@ -11,7 +11,7 @@
       "context": ".",
       "dockerfile": "Dockerfile",
       "tags": [
-        "test-tag:"
+        "test-tag:bar"
       ],
       "cache-from": [
         "type=local,src=./cache/multi-platform-demo"
```

The current state of things is that the `docker-compose` bake file format is not in feature parity with the HCL file format when it comes to the `.env` file.  Given that YAML and JSON could be considered as subsets of HCL, there will never be 100% feature parity.  So we shouldn't expect 100% parity given the nature of the two languages.  However, for simple `key = "value"` type expressions that are valid in both, it would make sense to support this (at least for `.env` files).

Currently, the command alternative: `docker buildx bake --file docker-compose.buildx.yml  --file .env` doesn't actually evaluate the `.env` variables file at all.

### Workaround

A hack-ish workaround that currently appears to work is to simply export the variables:

    eval $(sed -e '/^#/d' -e 's/^/export /' -e 's/$/;/' .env) ;
    docker buildx bake --file docker-compose.buildx.yml --print multi-platform-demo

This appears to work for simple variable substitutions, which get printed out in the `buildx bake` JSON output.

```bash
diff -Naur <(eval $(sed -e '/^#/d' -e 's/^/export /' -e 's/$/;/' .env) ; \
             docker buildx bake --file docker-compose.buildx.yml          --print multi-platform-demo) \
           <(docker buildx bake --file docker-buildx-bake.hcl --file .env --print multi-platform-demo)
[+] Building 0.0s (0/0)
[+] Building 0.0s (0/0)
## No difference in output now... thanks to exporting all variables in .env
```
@crazy-max: @trinitronx This should be fixed by https://github.com/docker/buildx/pull/905 (not released yet but you can [build from source](https://github.com/docker/buildx#building))@cdupont: I tried buildx v0.8.0 but this issue doesn't seem to be fixed.

docker-compose.yml:
```
version: "3.9"

services:
  foo:
    image: foo:$TAG
```

.env:
```
TAG=bar
```

```
$ docker buildx bake
[+] Building 0.0s (0/0)                                                                                                                        
WARNING: The "TAG" variable is not set. Defaulting to a blank string.
```



@olidroide: same to me @cdupont with dockerx v0.8.0 also v0.8.1

with docker-compose works perfect taking `.env` file from same directory.@eddietisma: I also encountered this issue using v.0.8.2. We're not ready to migrate to `.hcl` yet. Re-open?@marcellodesales: Sam problem here... The env var is not applied by default just like docker-compose does... @raphaelsetin: The above solutions didn't work for me, so I came up with the following:

```shell
# Parse the ".env" variables into the compatible "env.hcl" file
# Remove empty lines, then remove lines with these prohibited characters, then enclose all the values with double quotes
sed -e '/^[[:space:]]*$/d' -e '/[#@]/d' -e 's/\(^[^=]*\)=\(.*\)/\1="\2"/' .env > env.hcl

# Build the images with the "env.hcl" override
docker buildx bake -f docker-bake.hcl -f env.hcl --load
```@esemeniuc: Same issue here!@MidasLamb: I think a problem that still exists with .env file not being loaded as environment variables is that if you specify secrets that are read from the environment. Even with moving it into a env.hcl, the problem remains that the expected env variables aren't present during building. In a company setting this is annoying since a lot of the packages etc pulled in during build time requires authentication.@MalteMagnussen: Is this now the case, as implied by https://docs.docker.com/build/bake/compose-file/#env-file ? 

Solution patch:
diff --git a/bake/bake.go b/bake/bake.go
index 5f931572..573556cb 100644
--- a/bake/bake.go
+++ b/bake/bake.go
@@ -179,6 +179,21 @@ func dedupMap(ms ...map[string]string) map[string]string {
 	return res
 }
 
+func sliceToMap(env []string) (res map[string]string) {
+	res = make(map[string]string)
+	for _, s := range env {
+		kv := strings.SplitN(s, "=", 2)
+		key := kv[0]
+		switch {
+		case len(kv) == 1:
+			res[key] = ""
+		default:
+			res[key] = kv[1]
+		}
+	}
+	return
+}
+
 func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) {
 	defer func() {
 		err = formatHCLError(err, files)
@@ -242,15 +257,22 @@ func ParseFile(dt []byte, fn string) (*Config, error) {
 }
 
 func ParseComposeFile(dt []byte, fn string) (*Config, bool, error) {
+	envs := sliceToMap(os.Environ())
+	if wd, err := os.Getwd(); err == nil {
+		envs, err = loadDotEnv(envs, wd)
+		if err != nil {
+			return nil, true, err
+		}
+	}
 	fnl := strings.ToLower(fn)
 	if strings.HasSuffix(fnl, ".yml") || strings.HasSuffix(fnl, ".yaml") {
-		cfg, err := ParseCompose(dt)
+		cfg, err := ParseCompose(dt, envs)
 		return cfg, true, err
 	}
 	if strings.HasSuffix(fnl, ".json") || strings.HasSuffix(fnl, ".hcl") {
 		return nil, false, nil
 	}
-	cfg, err := ParseCompose(dt)
+	cfg, err := ParseCompose(dt, envs)
 	return cfg, err == nil, err
 }
 
diff --git a/bake/compose.go b/bake/compose.go
index 1f0a3b5d..d89c5d8a 100644
--- a/bake/compose.go
+++ b/bake/compose.go
@@ -3,8 +3,10 @@ package bake
 import (
 	"fmt"
 	"os"
+	"path/filepath"
 	"strings"
 
+	"github.com/compose-spec/compose-go/dotenv"
 	"github.com/compose-spec/compose-go/loader"
 	compose "github.com/compose-spec/compose-go/types"
 	"github.com/pkg/errors"
@@ -14,34 +16,18 @@ import (
 // errComposeInvalid is returned when a compose file is invalid
 var errComposeInvalid = errors.New("invalid compose file")
 
-func parseCompose(dt []byte) (*compose.Project, error) {
-	return loader.Load(compose.ConfigDetails{
+func ParseCompose(dt []byte, envs map[string]string) (*Config, error) {
+	cfg, err := loader.Load(compose.ConfigDetails{
 		ConfigFiles: []compose.ConfigFile{
 			{
 				Content: dt,
 			},
 		},
-		Environment: envMap(os.Environ()),
+		Environment: envs,
 	}, func(options *loader.Options) {
 		options.SkipNormalization = true
 		options.SkipConsistencyCheck = true
 	})
-}
-
-func envMap(env []string) map[string]string {
-	result := make(map[string]string, len(env))
-	for _, s := range env {
-		kv := strings.SplitN(s, "=", 2)
-		if len(kv) != 2 {
-			continue
-		}
-		result[kv[0]] = kv[1]
-	}
-	return result
-}
-
-func ParseCompose(dt []byte) (*Config, error) {
-	cfg, err := parseCompose(dt)
 	if err != nil {
 		return nil, err
 	}
@@ -124,6 +110,42 @@ func ParseCompose(dt []byte) (*Config, error) {
 	return &c, nil
 }
 
+func loadDotEnv(curenv map[string]string, workingDir string) (map[string]string, error) {
+	if curenv == nil {
+		curenv = make(map[string]string)
+	}
+
+	ef, err := filepath.Abs(filepath.Join(workingDir, ".env"))
+	if err != nil {
+		return nil, err
+	}
+
+	if _, err = os.Stat(ef); os.IsNotExist(err) {
+		return curenv, nil
+	} else if err != nil {
+		return nil, err
+	}
+
+	dt, err := os.ReadFile(ef)
+	if err != nil {
+		return nil, err
+	}
+
+	envs, err := dotenv.UnmarshalBytes(dt)
+	if err != nil {
+		return nil, err
+	}
+
+	for k, v := range envs {
+		if _, set := curenv[k]; set {
+			continue
+		}
+		curenv[k] = v
+	}
+
+	return curenv, nil
+}
+
 func flatten(in compose.MappingWithEquals) compose.Mapping {
 	if len(in) == 0 {
 		return nil
diff --git a/docs/guides/bake/compose-file.md b/docs/guides/bake/compose-file.md
index 5d40a8c7..6fcc43cb 100644
--- a/docs/guides/bake/compose-file.md
+++ b/docs/guides/bake/compose-file.md
@@ -94,6 +94,56 @@ limitations with the compose format:
 * Specifying variables or global scope attributes is not yet supported
 * `inherits` service field is not supported, but you can use [YAML anchors](https://docs.docker.com/compose/compose-file/#fragments) to reference other services like the example above
 
+## `.env` file
+
+You can declare default environment variables in an environment file named
+`.env`. This file will be loaded from the current working directory,
+where the command is executed and applied to compose definitions passed
+with `-f`.
+
+```yaml
+# docker-compose.yml
+services:
+  webapp:
+    image: docker.io/username/webapp:${TAG:-v1.0.0}
+    build:
+      dockerfile: Dockerfile
+```
+
+```
+# .env
+TAG=v1.1.0
+```
+
+```console
+$ docker buildx bake --print
+```
+```json
+{
+  "group": {
+    "default": {
+      "targets": [
+        "webapp"
+      ]
+    }
+  },
+  "target": {
+    "webapp": {
+      "context": ".",
+      "dockerfile": "Dockerfile",
+      "tags": [
+        "docker.io/username/webapp:v1.1.0"
+      ]
+    }
+  }
+}
+```
+
+> **Note**
+>
+> System environment variables take precedence over environment variables
+> in `.env` file.
+
 ## Extension field with `x-bake`
 
 Even if some fields are not (yet) available in the compose specification, you



Test patch:
diff --git a/bake/compose_test.go b/bake/compose_test.go
index 4f370c29..4a075ab2 100644
--- a/bake/compose_test.go
+++ b/bake/compose_test.go
@@ -2,6 +2,7 @@ package bake
 
 import (
 	"os"
+	"path/filepath"
 	"sort"
 	"testing"
 
@@ -37,11 +38,11 @@ secrets:
     file: /root/.aws/credentials
 `)
 
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 
 	require.Equal(t, 1, len(c.Groups))
-	require.Equal(t, c.Groups[0].Name, "default")
+	require.Equal(t, "default", c.Groups[0].Name)
 	sort.Strings(c.Groups[0].Targets)
 	require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets)
 
@@ -58,8 +59,8 @@ secrets:
 	require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile)
 	require.Equal(t, 1, len(c.Targets[1].Args))
 	require.Equal(t, "123", c.Targets[1].Args["buildno"])
-	require.Equal(t, c.Targets[1].CacheFrom, []string{"type=local,src=path/to/cache"})
-	require.Equal(t, c.Targets[1].CacheTo, []string{"type=local,dest=path/to/cache"})
+	require.Equal(t, []string{"type=local,src=path/to/cache"}, c.Targets[1].CacheFrom)
+	require.Equal(t, []string{"type=local,dest=path/to/cache"}, c.Targets[1].CacheTo)
 	require.Equal(t, "none", *c.Targets[1].NetworkMode)
 	require.Equal(t, []string{
 		"id=token,env=ENV_TOKEN",
@@ -75,7 +76,7 @@ services:
     webapp:
         build: ./db
 `)
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 	require.Equal(t, 1, len(c.Groups))
 }
@@ -93,7 +94,7 @@ services:
       target: webapp
 `)
 
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 
 	require.Equal(t, 2, len(c.Targets))
@@ -118,15 +119,15 @@ services:
       target: webapp
 `)
 
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 	require.Equal(t, 2, len(c.Targets))
 	sort.Slice(c.Targets, func(i, j int) bool {
 		return c.Targets[i].Name < c.Targets[j].Name
 	})
-	require.Equal(t, c.Targets[0].Name, "db")
+	require.Equal(t, "db", c.Targets[0].Name)
 	require.Equal(t, "db", *c.Targets[0].Target)
-	require.Equal(t, c.Targets[1].Name, "webapp")
+	require.Equal(t, "webapp", c.Targets[1].Name)
 	require.Equal(t, "webapp", *c.Targets[1].Target)
 }
 
@@ -152,11 +153,11 @@ services:
 	os.Setenv("ZZZ_BAR", "zzz_foo")
 	defer os.Unsetenv("ZZZ_BAR")
 
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, sliceToMap(os.Environ()))
 	require.NoError(t, err)
-	require.Equal(t, c.Targets[0].Args["FOO"], "bar")
-	require.Equal(t, c.Targets[0].Args["BAR"], "zzz_foo")
-	require.Equal(t, c.Targets[0].Args["BRB"], "FOO")
+	require.Equal(t, "bar", c.Targets[0].Args["FOO"])
+	require.Equal(t, "zzz_foo", c.Targets[0].Args["BAR"])
+	require.Equal(t, "FOO", c.Targets[0].Args["BRB"])
 }
 
 func TestInconsistentComposeFile(t *testing.T) {
@@ -166,7 +167,7 @@ services:
     entrypoint: echo 1
 `)
 
-	_, err := ParseCompose(dt)
+	_, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 }
 
@@ -191,7 +192,7 @@ networks:
           gateway: 10.5.0.254
 `)
 
-	_, err := ParseCompose(dt)
+	_, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 }
 
@@ -208,9 +209,9 @@ services:
         - bar
 `)
 
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
-	require.Equal(t, c.Targets[0].Tags, []string{"foo", "bar"})
+	require.Equal(t, []string{"foo", "bar"}, c.Targets[0].Tags)
 }
 
 func TestDependsOnList(t *testing.T) {
@@ -245,7 +246,7 @@ networks:
     name: test-net
 `)
 
-	_, err := ParseCompose(dt)
+	_, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 }
 
@@ -298,25 +299,25 @@ services:
         no-cache: true
 `)
 
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 	require.Equal(t, 2, len(c.Targets))
 	sort.Slice(c.Targets, func(i, j int) bool {
 		return c.Targets[i].Name < c.Targets[j].Name
 	})
-	require.Equal(t, c.Targets[0].Args, map[string]string{"CT_ECR": "foo", "CT_TAG": "bar"})
-	require.Equal(t, c.Targets[0].Tags, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"})
-	require.Equal(t, c.Targets[0].Platforms, []string{"linux/amd64", "linux/arm64"})
-	require.Equal(t, c.Targets[0].CacheFrom, []string{"user/app:cache", "type=local,src=path/to/cache"})
-	require.Equal(t, c.Targets[0].CacheTo, []string{"user/app:cache", "type=local,dest=path/to/cache"})
-	require.Equal(t, c.Targets[0].Pull, newBool(true))
-	require.Equal(t, c.Targets[0].Contexts, map[string]string{"alpine": "docker-image://alpine:3.13"})
-	require.Equal(t, c.Targets[1].Tags, []string{"ct-fake-aws:bar"})
-	require.Equal(t, c.Targets[1].Secrets, []string{"id=mysecret,src=/local/secret", "id=mysecret2,src=/local/secret2"})
-	require.Equal(t, c.Targets[1].SSH, []string{"default"})
-	require.Equal(t, c.Targets[1].Platforms, []string{"linux/arm64"})
-	require.Equal(t, c.Targets[1].Outputs, []string{"type=docker"})
-	require.Equal(t, c.Targets[1].NoCache, newBool(true))
+	require.Equal(t, map[string]string{"CT_ECR": "foo", "CT_TAG": "bar"}, c.Targets[0].Args)
+	require.Equal(t, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"}, c.Targets[0].Tags)
+	require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[0].Platforms)
+	require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom)
+	require.Equal(t, []string{"user/app:cache", "type=local,dest=path/to/cache"}, c.Targets[0].CacheTo)
+	require.Equal(t, newBool(true), c.Targets[0].Pull)
+	require.Equal(t, map[string]string{"alpine": "docker-image://alpine:3.13"}, c.Targets[0].Contexts)
+	require.Equal(t, []string{"ct-fake-aws:bar"}, c.Targets[1].Tags)
+	require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,src=/local/secret2"}, c.Targets[1].Secrets)
+	require.Equal(t, []string{"default"}, c.Targets[1].SSH)
+	require.Equal(t, []string{"linux/arm64"}, c.Targets[1].Platforms)
+	require.Equal(t, []string{"type=docker"}, c.Targets[1].Outputs)
+	require.Equal(t, newBool(true), c.Targets[1].NoCache)
 }
 
 func TestComposeExtDedup(t *testing.T) {
@@ -342,12 +343,12 @@ services:
           - type=local,dest=path/to/cache
 `)
 
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 	require.Equal(t, 1, len(c.Targets))
-	require.Equal(t, c.Targets[0].Tags, []string{"ct-addon:foo", "ct-addon:baz"})
-	require.Equal(t, c.Targets[0].CacheFrom, []string{"user/app:cache", "type=local,src=path/to/cache"})
-	require.Equal(t, c.Targets[0].CacheTo, []string{"user/app:cache", "type=local,dest=path/to/cache"})
+	require.Equal(t, []string{"ct-addon:foo", "ct-addon:baz"}, c.Targets[0].Tags)
+	require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom)
+	require.Equal(t, []string{"user/app:cache", "type=local,dest=path/to/cache"}, c.Targets[0].CacheTo)
 }
 
 func TestEnv(t *testing.T) {
@@ -375,9 +376,30 @@ services:
       - ` + envf.Name() + `
 `)
 
-	c, err := ParseCompose(dt)
+	c, err := ParseCompose(dt, nil)
+	require.NoError(t, err)
+	require.Equal(t, map[string]string{"CT_ECR": "foo", "FOO": "bsdf -csdf", "NODE_ENV": "test"}, c.Targets[0].Args)
+}
+
+func TestDotEnv(t *testing.T) {
+	tmpdir := t.TempDir()
+
+	err := os.WriteFile(filepath.Join(tmpdir, ".env"), []byte("FOO=bar"), 0644)
+	require.NoError(t, err)
+
+	var dt = []byte(`
+services:
+  scratch:
+    build:
+     context: .
+     args:
+        FOO:
+`)
+
+	chdir(t, tmpdir)
+	c, _, err := ParseComposeFile(dt, "docker-compose.yml")
 	require.NoError(t, err)
-	require.Equal(t, c.Targets[0].Args, map[string]string{"CT_ECR": "foo", "FOO": "bsdf -csdf", "NODE_ENV": "test"})
+	require.Equal(t, map[string]string{"FOO": "bar"}, c.Targets[0].Args)
 }
 
 func TestPorts(t *testing.T) {
@@ -397,7 +419,7 @@ services:
         published: "3306"
         protocol: tcp
 `)
-	_, err := ParseCompose(dt)
+	_, err := ParseCompose(dt, nil)
 	require.NoError(t, err)
 }
 
@@ -445,10 +467,10 @@ func TestServiceName(t *testing.T) {
 		t.Run(tt.svc, func(t *testing.T) {
 			_, err := ParseCompose([]byte(`
 services:
-  ` + tt.svc + `:
+  `+tt.svc+`:
     build:
       context: .
-`))
+`), nil)
 			if tt.wantErr {
 				require.Error(t, err)
 			} else {
@@ -514,7 +536,7 @@ services:
 	for _, tt := range cases {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := ParseCompose(tt.dt)
+			_, err := ParseCompose(tt.dt, nil)
 			if tt.wantErr {
 				require.Error(t, err)
 			} else {
@@ -523,3 +545,21 @@ services:
 		})
 	}
 }
+
+// chdir changes the current working directory to the named directory,
+// and then restore the original working directory at the end of the test.
+func chdir(t *testing.T, dir string) {
+	olddir, err := os.Getwd()
+	if err != nil {
+		t.Fatalf("chdir: %v", err)
+	}
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("chdir %s: %v", dir, err)
+	}
+	t.Cleanup(func() {
+		if err := os.Chdir(olddir); err != nil {
+			t.Errorf("chdir to original working directory %s: %v", olddir, err)
+			os.Exit(1)
+		}
+	})
+}