diff --git a/src/baudolo/backup/compose.py b/src/baudolo/backup/compose.py
index 487209d..6a2a011 100644
--- a/src/baudolo/backup/compose.py
+++ b/src/baudolo/backup/compose.py
@@ -4,88 +4,30 @@ import os
import shutil
import subprocess
from pathlib import Path
-from typing import List, Optional
-
-
-def _detect_env_file(project_dir: Path) -> Optional[Path]:
- """
- Detect Compose env file in a directory.
- Preference (same as Infinito.Nexus wrapper):
- 1)
/.env (file)
- 2) /.env/env (file) (legacy layout)
- """
- c1 = project_dir / ".env"
- if c1.is_file():
- return c1
-
- c2 = project_dir / ".env" / "env"
- if c2.is_file():
- return c2
-
- return None
-
-
-def _detect_compose_files(project_dir: Path) -> List[Path]:
- """
- Detect Compose file stack in a directory (same as Infinito.Nexus wrapper).
- Always requires docker-compose.yml.
- Optionals:
- - docker-compose.override.yml
- - docker-compose.ca.override.yml
- """
- base = project_dir / "docker-compose.yml"
- if not base.is_file():
- raise FileNotFoundError(f"Missing docker-compose.yml in: {project_dir}")
-
- files = [base]
-
- override = project_dir / "docker-compose.override.yml"
- if override.is_file():
- files.append(override)
-
- ca_override = project_dir / "docker-compose.ca.override.yml"
- if ca_override.is_file():
- files.append(ca_override)
-
- return files
-
-
-def _compose_wrapper_path() -> Optional[str]:
- """
- Prefer the Infinito.Nexus compose wrapper if present.
- Equivalent to: `which compose`
- """
- return shutil.which("compose")
+from typing import List
def _build_compose_cmd(project_dir: str, passthrough: List[str]) -> List[str]:
"""
Build the compose command for this project directory.
- Behavior:
- - If `compose` wrapper exists: use it with --chdir (so it resolves -f/--env-file itself)
- - Else: use `docker compose` and replicate wrapper's file/env detection.
+ Policy:
+ - If `compose` wrapper exists (Infinito.Nexus): use it and delegate ALL logic to it.
+ - Else: use plain `docker compose` with --chdir.
+ - NO custom compose file/env detection in this project.
"""
pdir = Path(project_dir).resolve()
- wrapper = _compose_wrapper_path()
+ wrapper = shutil.which("compose")
if wrapper:
- # Wrapper defaults project name to basename of --chdir.
# "--" ensures wrapper stops parsing its own args.
return [wrapper, "--chdir", str(pdir), "--", *passthrough]
- # Fallback: pure docker compose, but mirror wrapper behavior.
- files = _detect_compose_files(pdir)
- env_file = _detect_env_file(pdir)
+ docker = shutil.which("docker")
+ if docker:
+ return [docker, "compose", "--chdir", str(pdir), *passthrough]
- cmd: List[str] = ["docker", "compose"]
- for f in files:
- cmd += ["-f", str(f)]
- if env_file:
- cmd += ["--env-file", str(env_file)]
-
- cmd += passthrough
- return cmd
+ raise RuntimeError("Neither 'compose' nor 'docker' found in PATH")
def hard_restart_docker_services(dir_path: str) -> None:
diff --git a/tests/unit/backup/test_compose.py b/tests/unit/backup/test_compose.py
index d510c7c..d240dd1 100644
--- a/tests/unit/backup/test_compose.py
+++ b/tests/unit/backup/test_compose.py
@@ -53,52 +53,6 @@ class TestCompose(unittest.TestCase):
cls.compose_mod = mod
- def test_detect_env_file_prefers_dotenv_over_legacy(self) -> None:
- with tempfile.TemporaryDirectory() as td:
- tmp_path = Path(td)
- d = _setup_compose_dir(tmp_path, env_layout=".env/env")
- # Also create .env file -> should be preferred
- _touch(d / ".env")
-
- env_file = self.compose_mod._detect_env_file(d)
- self.assertEqual(env_file, d / ".env")
-
- def test_detect_env_file_uses_legacy_if_no_dotenv(self) -> None:
- with tempfile.TemporaryDirectory() as td:
- tmp_path = Path(td)
- d = _setup_compose_dir(tmp_path, env_layout=".env/env")
-
- env_file = self.compose_mod._detect_env_file(d)
- self.assertEqual(env_file, d / ".env" / "env")
-
- def test_detect_compose_files_requires_base(self) -> None:
- with tempfile.TemporaryDirectory() as td:
- tmp_path = Path(td)
- d = tmp_path / "stack"
- d.mkdir()
-
- with self.assertRaises(FileNotFoundError):
- self.compose_mod._detect_compose_files(d)
-
- def test_detect_compose_files_includes_optional_overrides(self) -> None:
- with tempfile.TemporaryDirectory() as td:
- tmp_path = Path(td)
- d = _setup_compose_dir(
- tmp_path,
- with_override=True,
- with_ca_override=True,
- )
-
- files = self.compose_mod._detect_compose_files(d)
- self.assertEqual(
- files,
- [
- d / "docker-compose.yml",
- d / "docker-compose.override.yml",
- d / "docker-compose.ca.override.yml",
- ],
- )
-
def test_build_cmd_uses_wrapper_when_present(self) -> None:
with tempfile.TemporaryDirectory() as td:
tmp_path = Path(td)
@@ -106,9 +60,12 @@ class TestCompose(unittest.TestCase):
tmp_path, with_override=True, with_ca_override=True, env_layout=".env"
)
- with patch.object(
- self.compose_mod.shutil, "which", lambda name: "/usr/local/bin/compose"
- ):
+ def fake_which(name: str):
+ if name == "compose":
+ return "/usr/local/bin/compose"
+ return None
+
+ with patch.object(self.compose_mod.shutil, "which", fake_which):
cmd = self.compose_mod._build_compose_cmd(str(d), ["up", "-d"])
self.assertEqual(
@@ -123,7 +80,7 @@ class TestCompose(unittest.TestCase):
],
)
- def test_build_cmd_fallback_docker_compose_with_all_files_and_env(self) -> None:
+ def test_build_cmd_fallback_uses_plain_docker_compose_chdir(self) -> None:
with tempfile.TemporaryDirectory() as td:
tmp_path = Path(td)
d = _setup_compose_dir(
@@ -133,22 +90,23 @@ class TestCompose(unittest.TestCase):
env_layout=".env",
)
- with patch.object(self.compose_mod.shutil, "which", lambda name: None):
+ def fake_which(name: str):
+ if name == "compose":
+ return None
+ if name == "docker":
+ return "/usr/bin/docker"
+ return None
+
+ with patch.object(self.compose_mod.shutil, "which", fake_which):
cmd = self.compose_mod._build_compose_cmd(
str(d), ["up", "-d", "--force-recreate"]
)
expected: List[str] = [
- "docker",
+ "/usr/bin/docker",
"compose",
- "-f",
- str((d / "docker-compose.yml").resolve()),
- "-f",
- str((d / "docker-compose.override.yml").resolve()),
- "-f",
- str((d / "docker-compose.ca.override.yml").resolve()),
- "--env-file",
- str((d / ".env").resolve()),
+ "--chdir",
+ str(d.resolve()),
"up",
"-d",
"--force-recreate",
@@ -160,9 +118,12 @@ class TestCompose(unittest.TestCase):
tmp_path = Path(td)
d = _setup_compose_dir(tmp_path, name="mailu", env_layout=".env")
- with patch.object(
- self.compose_mod.shutil, "which", lambda name: "/usr/local/bin/compose"
- ):
+ def fake_which(name: str):
+ if name == "compose":
+ return "/usr/local/bin/compose"
+ return None
+
+ with patch.object(self.compose_mod.shutil, "which", fake_which):
calls = []
def fake_run(cmd, check: bool):
@@ -210,7 +171,14 @@ class TestCompose(unittest.TestCase):
env_layout=".env/env",
)
- with patch.object(self.compose_mod.shutil, "which", lambda name: None):
+ def fake_which(name: str):
+ if name == "compose":
+ return None
+ if name == "docker":
+ return "/usr/bin/docker"
+ return None
+
+ with patch.object(self.compose_mod.shutil, "which", fake_which):
calls = []
def fake_run(cmd, check: bool):
@@ -220,19 +188,32 @@ class TestCompose(unittest.TestCase):
with patch.object(self.compose_mod.subprocess, "run", fake_run):
self.compose_mod.hard_restart_docker_services(str(d))
- down_cmd = calls[0][0]
- up_cmd = calls[1][0]
-
- self.assertTrue(calls[0][1] is True)
- self.assertTrue(calls[1][1] is True)
-
- self.assertEqual(down_cmd[0:2], ["docker", "compose"])
- self.assertEqual(down_cmd[-1], "down")
- self.assertIn("--env-file", down_cmd)
-
- self.assertEqual(up_cmd[0:2], ["docker", "compose"])
- self.assertTrue(up_cmd[-2:] == ["up", "-d"] or up_cmd[-3:] == ["up", "-d"])
- self.assertIn("--env-file", up_cmd)
+ self.assertEqual(
+ calls,
+ [
+ (
+ [
+ "/usr/bin/docker",
+ "compose",
+ "--chdir",
+ str(d.resolve()),
+ "down",
+ ],
+ True,
+ ),
+ (
+ [
+ "/usr/bin/docker",
+ "compose",
+ "--chdir",
+ str(d.resolve()),
+ "up",
+ "-d",
+ ],
+ True,
+ ),
+ ],
+ )
if __name__ == "__main__":