Repository: docker/compose-cli
Pull request #2178 (2022-07-26 20:20:49)

cli binary path handling
**What I did**

Update the way we choose which `com.docker.cli` binary executable to use.

1. Use path from env variable `$DOCKER_COM_DOCKER_CLI` if defined and non-empty
2. Use path formed from parent directory of current process executable joined with
   `com.docker.cli`
3. Look for `com.docker.cli` on $PATH


**Related issue**
<!-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" -->

<!-- optional tests
You can add a / mention to run tests executed by default only on main branch :
* `test-kube` to run Kube E2E tests
* `test-aci` to run ACI E2E tests
* `test-ecs` to run ECS E2E tests
* `test-windows` to run tests & E2E tests on windows
-->

**(not mandatory) A picture of a cute animal, if possible in relation with what you did**

![Butterfly](https://images.pexels.com/photos/462118/pexels-photo-462118.jpeg?auto=compress&w=720)



Patch:
diff --git a/cli/mobycli/exec.go b/cli/mobycli/exec.go
index eecf9b88..9e5be867 100644
--- a/cli/mobycli/exec.go
+++ b/cli/mobycli/exec.go
@@ -24,6 +24,7 @@ import (
 	"os/signal"
 	"path/filepath"
 	"regexp"
+	"runtime"
 
 	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/docker/compose/v2/pkg/utils"
@@ -38,7 +39,13 @@ import (
 var delegatedContextTypes = []string{store.DefaultContextType}
 
 // ComDockerCli name of the classic cli binary
-const ComDockerCli = "com.docker.cli"
+var ComDockerCli = "com.docker.cli"
+
+func init() {
+	if runtime.GOOS == "windows" {
+		ComDockerCli += ".exe"
+	}
+}
 
 // ExecIfDefaultCtxType delegates to com.docker.cli if on moby context
 func ExecIfDefaultCtxType(ctx context.Context, root *cobra.Command) {
@@ -92,16 +99,7 @@ func Exec(root *cobra.Command) {
 
 // RunDocker runs a docker command, and forward signals to the shellout command (stops listening to signals when an event is sent to childExit)
 func RunDocker(childExit chan bool, args ...string) error {
-	execBinary, err := resolvepath.LookPath(ComDockerCli)
-	if err != nil {
-		execBinary = findBinary(ComDockerCli)
-		if execBinary == "" {
-			fmt.Fprintln(os.Stderr, err)
-			fmt.Fprintln(os.Stderr, "Current PATH : "+os.Getenv("PATH"))
-			os.Exit(1)
-		}
-	}
-	cmd := exec.Command(execBinary, args...)
+	cmd := exec.Command(comDockerCli(), args...)
 	cmd.Stdin = os.Stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
@@ -131,6 +129,25 @@ func RunDocker(childExit chan bool, args ...string) error {
 	return cmd.Run()
 }
 
+func comDockerCli() string {
+	if v := os.Getenv("DOCKER_COM_DOCKER_CLI"); v != "" {
+		return v
+	}
+
+	execBinary := findBinary(ComDockerCli)
+	if execBinary == "" {
+		var err error
+		execBinary, err = resolvepath.LookPath(ComDockerCli)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
+			fmt.Fprintln(os.Stderr, "Current PATH : "+os.Getenv("PATH"))
+			os.Exit(1)
+		}
+	}
+
+	return execBinary
+}
+
 func findBinary(filename string) string {
 	currentBinaryPath, err := os.Executable()
 	if err != nil {
@@ -149,7 +166,7 @@ func findBinary(filename string) string {
 
 // IsDefaultContextCommand checks if the command exists in the classic cli (issues a shellout --help)
 func IsDefaultContextCommand(dockerCommand string) bool {
-	cmd := exec.Command(ComDockerCli, dockerCommand, "--help")
+	cmd := exec.Command(comDockerCli(), dockerCommand, "--help")
 	b, e := cmd.CombinedOutput()
 	if e != nil {
 		fmt.Println(e)
@@ -162,7 +179,7 @@ func ExecSilent(ctx context.Context, args ...string) ([]byte, error) {
 	if len(args) == 0 {
 		args = os.Args[1:]
 	}
-	cmd := exec.CommandContext(ctx, ComDockerCli, args...)
+	cmd := exec.CommandContext(ctx, comDockerCli(), args...)
 	cmd.Stderr = os.Stderr
 	return cmd.Output()
 }


Test patch:
diff --git a/cli/mobycli/exec_test.go b/cli/mobycli/exec_test.go
index 7138464f..b9109306 100644
--- a/cli/mobycli/exec_test.go
+++ b/cli/mobycli/exec_test.go
@@ -17,11 +17,14 @@
 package mobycli
 
 import (
+	"os"
+	"path/filepath"
 	"testing"
 
 	"gotest.tools/v3/assert"
 
 	"github.com/docker/compose-cli/api/context/store"
+	"github.com/stretchr/testify/require"
 )
 
 func TestDelegateContextTypeToMoby(t *testing.T) {
@@ -44,3 +47,44 @@ func TestDelegateContextTypeToMoby(t *testing.T) {
 		assert.Assert(t, !mustDelegateToMoby(ctx))
 	}
 }
+
+func TestFindComDockerCli(t *testing.T) {
+	tmp := t.TempDir()
+	bin := filepath.Join(tmp, "bin")
+
+	// com.docker.cli on path
+	pathFile := filepath.Join(bin, ComDockerCli)
+	require.NoError(t, os.MkdirAll(bin, os.ModePerm))
+	require.NoError(t, os.WriteFile(pathFile, []byte(""), os.ModePerm))
+
+	exe, err := os.Executable()
+	require.NoError(t, err)
+	exe, err = filepath.EvalSymlinks(exe)
+	require.NoError(t, err)
+
+	// com.docker.cli in same directory as current executable
+	currFile := filepath.Join(filepath.Dir(exe), ComDockerCli)
+	require.NoError(t, os.WriteFile(currFile, []byte(""), os.ModePerm))
+
+	savePath := os.Getenv("PATH")
+	t.Cleanup(func() { _ = os.Setenv("PATH", savePath) })
+
+	_ = os.Setenv("PATH", bin)
+
+	t.Run("from $DOCKER_COM_DOCKER_CLI", func(t *testing.T) {
+		fromenv := filepath.Join(tmp, "fromenv")
+		_ = os.Setenv("DOCKER_COM_DOCKER_CLI", fromenv)
+		t.Cleanup(func() { _ = os.Unsetenv("DOCKER_COM_DOCKER_CLI") })
+
+		assert.Equal(t, fromenv, comDockerCli())
+	})
+
+	t.Run("from binary next to current executable", func(t *testing.T) {
+		assert.Equal(t, currFile, comDockerCli())
+	})
+
+	t.Run("from binary on path", func(t *testing.T) {
+		_ = os.Remove(currFile)
+		assert.Equal(t, pathFile, comDockerCli())
+	})
+}